Skip to main content

wavekit

CI PyPI version Python Versions Downloads License

English | 中文

wavekit is a high-performance Python library for digital waveform analysis. It loads VCD, FST, and FSDB signals into NumPy-backed Waveform objects, and builds on them with efficient signal processing, protocol analysis, and automated verification APIs.

🤖 AI integration: wavekit-mcp provides an MCP server for AI-assisted waveform analysis: load signals and run pattern matching from AI tools without hand-written scripts.

Features

  • Batch signal extraction: use brace expansion, integer ranges, regex, and wildcards to load related signals in one call.
  • Rich analysis API: use NumPy-style arithmetic, masking, bit slicing, edge detection, and time/cycle slicing to build signal queries in a few lines.
  • Temporal pattern matching: use declarative and programmable Pattern APIs to extract protocol transactions, measure handshake latency, and detect timeout or hang failures.
  • High-performance waveform processing: read VCD, FST, and FSDB files into compact NumPy-backed arrays for fast loading and efficient memory use.

Installation

pip install wavekit

Note: To read FSDB files, the Verdi runtime library (libNPI.so) must be available at runtime. Configure via:

  • WAVEKIT_NPI_LIB: direct path to libNPI.so
  • VERDI_HOME: Verdi installation directory (searches $VERDI_HOME/share/NPI/lib/...)
  • LD_LIBRARY_PATH: system library search path

Quick start

The examples below use placeholder filenames such as sim.vcd. Replace them with the path to your own VCD, FST, or FSDB file, and adjust signal paths to match your design hierarchy.

1. Batch Signal Extraction

Use brace expansion or regular expressions to load multiple related signals in one call.

from wavekit import VcdReader

with VcdReader("jtag.vcd") as f:
    # Keys contain BraceCapture objects whose groups are ("state",) / ("next",).
    waves = f.load_matched_waveforms(
        "tb.u0.J_{state,next}[3:0]",
        clock_path="tb.tck",
    )

    # /regex/ is the canonical regex syntax; groups are stored by RegexCapture.
    waves = f.load_matched_waveforms(
        r"tb.u0./J_([a-z]+)/",
        clock_path="tb.tck",
    )

2. Signal Analysis

Waveforms support NumPy-style arithmetic, masking, and edge detection.

import numpy as np
from wavekit import VcdReader

with VcdReader("fifo_tb.vcd") as f:
    clock = "fifo_tb.clk"
    depth = 8

    w_ptr = f.load_waveform("fifo_tb.s_fifo.w_ptr[2:0]", clock=clock)
    r_ptr = f.load_waveform("fifo_tb.s_fifo.r_ptr[2:0]", clock=clock)
    wr_en = f.load_waveform("fifo_tb.s_fifo.wr_en",      clock=clock)

    occupancy = (w_ptr + depth - r_ptr) % depth
    print(f"Average occupancy: {np.mean(occupancy.value):.2f}")

    # Filter to cycles where a write is active
    write_occ = occupancy.mask(wr_en == 1)

    # Detect write bursts
    burst_cycles = wr_en.rising_edge()

To inspect unknown/high-impedance source bits without changing the ordinary two-state value model, load an unsigned unknown mask alongside the value waveform. Each mask bit is 1 where the source sample contained X or Z.

Experimental: The load_unknown_mask / load_matched_unknown_masks APIs are experimental and may change in a future release.

from wavekit import VcdReader

with VcdReader("fifo_tb.vcd") as f:
    clock = "fifo_tb.clk"
    data = f.load_waveform("fifo_tb.s_fifo.data[7:0]", clock=clock, xz_value=0)
    unknown = f.load_unknown_mask("fifo_tb.s_fifo.data[7:0]", clock=clock)

    # Keep only samples whose source bits were fully known.
    known_data = data.mask(unknown == 0)

3. Expression Evaluation

Compute waveform expressions directly from signal path strings without loading each signal manually.

from wavekit import VcdReader

with VcdReader("fifo_tb.vcd") as f:
    # Single mode: paths must each match exactly one signal
    occupancy = f.eval(
        "fifo_tb.s_fifo.w_ptr[2:0] - fifo_tb.s_fifo.r_ptr[2:0]",
        clock="fifo_tb.clk",
    )

    # Zip mode: brace patterns expand per typed CaptureKey and evaluate per match.
    occupancies = f.eval(
        "tb.fifo_{0..3}.w_ptr[2:0] - tb.fifo_{0..3}.r_ptr[2:0]",
        clock="tb.clk",
        mode="zip",
    )

4. Pattern Matching

Pattern scans a waveform and extracts matching transactions, such as a request/response pair, a burst, a stall interval, or another repeating timing pattern.

Choose the form that matches the transaction shape:

  • Declarative: use .wait(), .consume(), .capture(), and .loop() steps to describe fixed flows.
  • Programmable: use a normal Python function when the flow depends on waveform values, such as dynamic branches or per-ID routing.

In declarative patterns, the first blocking step selects transaction start cycles; later blocking steps describe timing within each matched transaction.

match(pattern) returns MatchRecords, while collect(body) returns a Python list of extracted values.

Declarative examples

AXI-Lite read latency

from wavekit import VcdReader
from wavekit.pattern import Pattern, match, collect

with VcdReader("axi_tb.vcd") as f:
    clk     = "tb.clk"
    arvalid = f.load_waveform("tb.dut.arvalid",     clock=clk)
    arready = f.load_waveform("tb.dut.arready",     clock=clk)
    rvalid  = f.load_waveform("tb.dut.rvalid",      clock=clk)
    rready  = f.load_waveform("tb.dut.rready",      clock=clk)
    rdata   = f.load_waveform("tb.dut.rdata[31:0]", clock=clk)

    pattern = (
        Pattern()
        .wait(arvalid & arready)   # AR handshake → transaction starts
        .wait(rvalid  & rready)    # R  handshake → transaction ends
        .capture("rdata", rdata)
    )
    result = match(pattern)

    ok = result.filter_ok()
    latency = ok.end.clock - ok.start.clock
    print(f"Read latencies (cycles): {latency}")
    print(f"Read data: {ok.captures['rdata'].value}")

AXI write burst (multi-beat)

beat = Pattern().consume(wvalid & wready, channel="w").capture("beats", wdata, mode="list")

pattern = (
    Pattern()
    .wait(awvalid & awready)   # AW handshake → burst starts
    .loop(beat, until=wlast)   # collect each beat until wlast
)
result = match(pattern)

for i, inst in enumerate(result.filter_ok()):
    print(f"Burst {i}: {len(inst.captures['beats'])} beats")

Stall detection

stall = valid & (ready == 0)

pattern = (
    Pattern()
    .wait(stall.rising_edge())             # stall begins
    .loop(Pattern().delay(1), when=stall)  # keep waiting until stall ends
)
result = match(pattern)

stalls = result.filter_ok()
stall_cycles = stalls.duration.value - 1
print(f"Stall durations: {stall_cycles} cycles")

Programmable example

DMA-style command stream

Use programmable control flow when a command's opcode changes the following timing shape.

cmd_fire = cmd_valid & cmd_ready   # precompute outside the handler
w_fire = w_valid & w_ready
rsp_fire = rsp_valid & rsp_ready
r_fire = r_valid & r_ready

OP_READ = 0
OP_WRITE = 1

def read_dma_cmd(ctx):
    if not ctx.value(cmd_fire):
        return None

    op = int(ctx.value(cmd_op))
    addr = int(ctx.value(cmd_addr))
    length = int(ctx.value(cmd_len))

    if op == OP_WRITE:
        data = []
        for _ in range(length):
            ctx.consume(w_fire, channel="wdata")
            data.append(int(ctx.value(w_data)))

        ctx.consume(rsp_fire, channel="rsp")
        return {
            "op": "write",
            "addr": addr,
            "data": data,
            "status": int(ctx.value(rsp_status)),
        }

    if op == OP_READ:
        ctx.consume(rsp_fire, channel="rsp")
        data = []
        for _ in range(length):
            ctx.consume(r_fire, channel="rdata")
            data.append(int(ctx.value(r_data)))

        return {"op": "read", "addr": addr, "data": data}

    ctx.require(False, message=f"unknown DMA op {op}")
    return None

commands = collect(read_dma_cmd)
print(f"Captured {len(commands)} commands")

Some tips for programmable patterns:

  • Precompute fixed waveform expressions, such as fire = valid & ready, outside the handler so they are not rebuilt every cycle.
  • Start the handler with if ctx.value(fire): ... to test whether the current cycle starts a transaction, and return None otherwise.
  • Use ctx.try_consume(...) for non-blocking polling or arbitration between candidate channels. For a linear burst, ctx.consume(...) is more direct.
  • Add timeout=<cycles> only when a blocking step needs a wait bound. In match(), timeout becomes MatchStatus.Timeout(...); in collect(), it raises PatternError.

API reference

Reader

Method Description
VcdReader(file) / FstReader(file) / FsdbReader(file, *, quiet=True) Open a waveform file. Use as a context manager. FsdbReader requires Verdi runtime (WAVEKIT_NPI_LIB, VERDI_HOME, or LD_LIBRARY_PATH) and suppresses the NPI startup banner by default; pass quiet=False to keep it.
reader.load_waveform(signal, clock, ...) Load one signal sampled on every clock edge. Returns Waveform.
reader.load_unknown_mask(signal, clock, ...) Experimental. Load X/Z bit presence as an unsigned mask Waveform.
reader.load_matched_waveforms(signal_path, clock_path, ...) Batch-load matching signals. Returns dict[CaptureKey, Waveform].
reader.load_matched_unknown_masks(signal_path, clock_path, ...) Experimental. Batch-load X/Z masks for matched signals. Returns dict[CaptureKey, Waveform].
reader.eval(expr, clock, mode='single'|'zip', ...) Evaluate an arithmetic expression with embedded signal paths.
reader.get_matched_signals(path) Resolve a query to Signal objects without loading data.
reader.get_matched_scopes(path) Resolve a query to Scope objects.
reader.top_scopes Immutable tuple of root Scope nodes.
has_fsdb_support() Report whether the Verdi FSDB runtime is currently available.

Signal path patterns

Syntax Example Effect Capture in result key
Plain name tb.dut.valid Exact-name match None
{a,b,c} sig_{read,write} Enumerate named variants BraceCapture(path=..., groups=...)
{N..M} fifo_{0..3}.ptr Integer range BraceCapture(path=..., groups=...)
{N..M..step} lane_{0..6..2} Stepped range BraceCapture(path=..., groups=...)
/<regex>/ /([a-z]+)_valid/ Canonical regex syntax with capture groups RegexCapture(path=..., groups=...)
@<regex> @([a-z]+)_valid Legacy-compatible regex syntax RegexCapture(path=..., groups=...)
* / ** tb.*.valid / tb.**.valid Single-level / recursive wildcard WildcardCapture(path=...)
$ModName tb.$fifo_unit.ptr Match a direct-child scope by module/definition name (FSDB only) ExactCapture(path=..., definition=...)
$$ModName tb.$$fifo_unit.ptr Match any-depth descendant scope by module/definition name (FSDB only) ExactCapture(path=..., definition=...)

$ and $$ are path-step modifiers that can combine with exact-name, brace, and regex matchers. For example: tb.$/fifo_(in|out)/.data_{0..3}[7:0].

Matched-reader and hierarchy query APIs return dictionaries keyed by CaptureKey = tuple[Capture, ...]. Exact-name components are omitted, so a fully exact query uses key ().


Waveform

A Waveform wraps three parallel numpy arrays (.value, .clock, .time). All operations return a new Waveform.

Arithmetic & comparison: +, -, *, //, %, **, /, &, |, ^, ~, ==, !=, <<, >>

Filtering & slicing

Method Description
wave.mask(mask) Keep samples where a boolean Waveform or array is True
wave.filter(fn) Keep samples where fn(value) is True
wave.cycle_slice(begin, end) Trim to clock cycle range [begin, end)
wave.time_slice(begin, end) Trim to simulation time range
wave.slice(begin_idx, end_idx) Trim by array index
wave.take(indices) Select samples at given indices

Transformation

Method Description
wave.map(fn, width, signed) Element-wise transform
wave.unique_consecutive() Remove consecutive duplicate values
wave.compress() Compact a waveform while preserving value changes and the final sample
wave.downsample(chunk, fn) Aggregate into chunks
wave.as_signed() / wave.as_unsigned() Reinterpret signedness

Bit manipulation

Method / Syntax Description
wave[high:low] Extract bit field (Verilog convention, returns unsigned)
wave[n] Extract single bit
wave.split_bits(n) Split into n-bit groups (LSB first)
Waveform.concatenate([w0, w1, ...]) Concatenate (w0 = LSB)
wave.bit_count() Population count

Edge detection (1-bit only)

Method Description
wave.rising_edge() True at 0→1 transitions
wave.falling_edge() True at 1→0 transitions

Relative time access

Method Description
wave.relative(offset, pad, pad_value) Shift by offset cycles (positive = future, negative = past)
wave.ahead(n, pad, pad_value) Look n cycles into the future (shorthand for relative(n))
wave.back(n, pad, pad_value) Look n cycles into the past (shorthand for relative(-n))

pad controls boundary handling: 'repeat' (default) pads with the first/last value, 'value' pads with a given pad_value.

# Rising edge detection
rising = (wave == 0) & wave.ahead()

# Compare current vs 3 cycles ago
changed = wave != wave.back(3)

Pattern

Construction

API Description
Pattern() Create a declarative Pattern. Add steps with builder methods; execution options live on match() / collect().
match(pattern_or_body, *, axis=None, timeout=None, timeout_message=None, start_cycle=None, end_cycle=None) Run a declarative Pattern or programmable check body and return MatchRecords. Check bodies return ctx.OK or None.
collect(body, *, axis=None, timeout=None, timeout_message=None, start_cycle=None, end_cycle=None) Run a programmable extraction body and collect each non-None Python return value.

Declarative Steps

Method Description
.wait(cond, *, require=None, require_message=None) Block until cond is True without consuming the event. Resumes in the same cycle when already true; use .delay(1) for next-cycle behavior. require is checked each waiting cycle (failure → MatchStatus.RequireViolated).
.consume(cond, channel, *, require=None, require_message=None) Block until cond is True and this instance can exclusively consume from channel. Resumes in the same cycle on success. Use this for request/response pairing and per-key routing.
.delay(n, *, require=None, require_message=None) Advance n cycles. delay(0) is a no-op. require must hold every cycle.
.capture(name, signal, *, mode='last') Record signal value at current cycle. mode='last' (default) overwrites; 'first' keeps the first write; 'list' appends to a list.
.require(cond) Assert condition; fail with MatchStatus.RequireViolated if False.
.loop(body, *, until=None, when=None) until: do-while (exit when True after body). when: while (exit when False before body).
.repeat(body, n) Execute body exactly n times. n may be a callable.
.branch(cond, true_body, false_body) Conditional branch.

The same time and ownership operations are available inside Programmable patterns as ctx.wait(...), ctx.consume(...), and ctx.delay(...).

Programmable Context

API Description
ctx.value(waveform, offset=0) Read a scalar value at the current sample plus optional offset.
ctx.cycle(waveform, offset=0) Read the cycle number at the current sample plus optional offset.
ctx.time(waveform, offset=0) Read the timestamp at the current sample plus optional offset.
ctx.wait(cond, require=None, require_message=None) Observe cycles until cond is true; does not consume the event.
ctx.consume(cond, channel, require=None, require_message=None) Wait for cond and exclusively consume from channel.
ctx.try_consume(cond, channel) Poll channel without blocking. Returns True only when both the condition and channel are available.
ctx.delay(n, require=None, require_message=None) Advance n cycles.
ctx.capture(name, value, mode='last') Record a capture for programmable match().
ctx.OK Return from programmable match() to record a successful match.

Dynamic callbacks

Callback arguments depend on whether the callback is used in the declarative API or the programmable API:

  • In declarative APIs such as Pattern().wait(...) and Pattern().consume(...), callbacks receive (index, captures).
  • In programmable functions, callbacks passed to methods such as ctx.wait(...) and ctx.consume(...) take no arguments. If they need the current index, captures, or signal values, close over ctx.

Channels and consume vs. wait

wait() is observational: every matching instance can see the same event. consume() adds ownership: only one instance can claim a given (channel, cycle). On the same channel, matches with earlier start cycles claim available events first.

A Channel is an identity token for consume ownership. Pass a Channel object, a hashable key, or a dynamic callback to consume(..., channel=...). All instances sharing the same channel key compete for the same logical channel.

from collections import defaultdict
from wavekit.pattern import Channel, Pattern, match

# Multi-bank cache: each bank has its own response port, so two banks can return
# data in the same cycle. Per-bank channels let independent requests consume
# independent response streams.
banks = defaultdict(Channel)

pattern = (
    Pattern()
    .wait(req_valid)
    .capture('bank', req_addr & 1)
    .consume(
        lambda i, cap: bank_valid[cap['bank']].value[i],
        channel=lambda i, cap: banks[cap['bank']],
    )
    .capture('rdata',
        lambda i, cap: bank_data[cap['bank']].value[i])
)
result = match(pattern)

MatchRecords

Field Description
.start / .end Point Waveforms. .value is the waveform-array sample index, .clock is the absolute cycle, and .time is the simulation timestamp. End is inclusive.
.duration end.value - start.value + 1 sampled cycles.
.status MatchStatus.OK(), MatchStatus.Timeout(...), or MatchStatus.RequireViolated(...).
.captures dict[str, Waveform] of captured values.
.ok Boolean Waveform where status == MatchStatus.OK().
.failed Boolean Waveform where status != MatchStatus.OK().
.filter_ok() Return only OK matches.
.filter_status(status_class) Return only matches with the given status class, such as MatchStatus.Timeout.
.filter_failed() Return only non-OK matches.

MatchRecords[i] returns a single MatchRecord, and slices return another MatchRecords batch.


Development

This project uses Poetry for dependency management and packaging.

Setup

git clone https://github.com/cxzzzz/wavekit.git
cd wavekit
poetry install

Testing

Tests are located in the tests/ directory and run with pytest.

# Run all tests
poetry run pytest

# Run a specific test file
poetry run pytest tests/test_pattern.py

# Run with verbose output
poetry run pytest -v

Linting & Formatting

This project uses Ruff for linting and formatting.

# Check for lint errors
poetry run ruff check .

# Check formatting (no changes)
poetry run ruff format --check .

# Auto-fix formatting
poetry run ruff format .

Type Checking

poetry run mypy .

Contributing

Issues and pull requests are welcome. Before sending a pull request, run the tests and format checks:

poetry run pytest
poetry run ruff check .
poetry run ruff format --check .

License

This project is licensed under the MIT License. See the LICENSE file for details.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wavekit-0.7.2.tar.gz (66.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

wavekit-0.7.2-cp314-cp314-manylinux_2_39_x86_64.whl (759.7 kB view details)

Uploaded CPython 3.14manylinux: glibc 2.39+ x86-64

wavekit-0.7.2-cp313-cp313-manylinux_2_39_x86_64.whl (761.3 kB view details)

Uploaded CPython 3.13manylinux: glibc 2.39+ x86-64

wavekit-0.7.2-cp312-cp312-manylinux_2_39_x86_64.whl (773.7 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.39+ x86-64

wavekit-0.7.2-cp311-cp311-manylinux_2_39_x86_64.whl (752.3 kB view details)

Uploaded CPython 3.11manylinux: glibc 2.39+ x86-64

wavekit-0.7.2-cp310-cp310-manylinux_2_39_x86_64.whl (721.3 kB view details)

Uploaded CPython 3.10manylinux: glibc 2.39+ x86-64

wavekit-0.7.2-cp39-cp39-manylinux_2_39_x86_64.whl (720.1 kB view details)

Uploaded CPython 3.9manylinux: glibc 2.39+ x86-64

File details

Details for the file wavekit-0.7.2.tar.gz.

File metadata

  • Download URL: wavekit-0.7.2.tar.gz
  • Upload date:
  • Size: 66.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for wavekit-0.7.2.tar.gz
Algorithm Hash digest
SHA256 ccaa227772c498e0007f40d2742d65a58544b9e7e0d068552945f26252ba3036
MD5 a960a3faf70a7d88941b64521d5c5d65
BLAKE2b-256 226242a5ad98c66963e5247abecb259156a42fa38da337d4614ebb389b4ee24b

See more details on using hashes here.

File details

Details for the file wavekit-0.7.2-cp314-cp314-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for wavekit-0.7.2-cp314-cp314-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 581cea3624626fd5f3f3b17740a7175c09598ee11cbaf5216123e047dad84229
MD5 186497750728340c748aeb2516de8531
BLAKE2b-256 60e5ec7cb58a715e3f3d4b265f027b736a6f6a91cbcd715c386be185ba628b2e

See more details on using hashes here.

File details

Details for the file wavekit-0.7.2-cp313-cp313-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for wavekit-0.7.2-cp313-cp313-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 d287dee16b180f6a5e21cad00a5d1e2ee7c87a395719eb60e299b6c4eaffc715
MD5 bcad940c5ead343b3642609f38f10689
BLAKE2b-256 d422a334c42549a22767824179862468196c573f55147a7ab0bedc341985d1ab

See more details on using hashes here.

File details

Details for the file wavekit-0.7.2-cp312-cp312-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for wavekit-0.7.2-cp312-cp312-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 24fe620e606ca169029f66d4f363bb60dc537ef2c6ce9fb6c98313ff0149278c
MD5 2d3d091712b234088b3b7634e73ac6f3
BLAKE2b-256 fa3cd92179ea263c7c38204d22b876e6abc6db89b5b0a732ef08a402a5dc2ab1

See more details on using hashes here.

File details

Details for the file wavekit-0.7.2-cp311-cp311-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for wavekit-0.7.2-cp311-cp311-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 166fccf9c6fd1a897d52a69264790ae6af322a2dfd8a08d1b3c9e2ff6b018f53
MD5 5ead26d88d8cf8a53ab69f383c24e57a
BLAKE2b-256 3f7acee809b9c9594e20f586623475c0a2a93ceecbd4da15efd9a095ed3da460

See more details on using hashes here.

File details

Details for the file wavekit-0.7.2-cp310-cp310-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for wavekit-0.7.2-cp310-cp310-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 95402c446dea5b5ff1a80a8975c038978d5744d92559c2de6a5074155c22f71e
MD5 a6b7f96519ba246048e533df1185b59e
BLAKE2b-256 b1894547687607da7948ff7bed94c90d3d1d1be261fd797b4a00fd4c8e45e3d7

See more details on using hashes here.

File details

Details for the file wavekit-0.7.2-cp39-cp39-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for wavekit-0.7.2-cp39-cp39-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 fd5ef63da445d397396f48c80fd5600c0057d954bfdf2a49a07bb3d3ec2fa375
MD5 141f74d3d57157134f6be9e0fb3e8b54
BLAKE2b-256 8334c5cc7c409995591b8fb2c0facd6757537c5f974d644c5543087df49c324a

See more details on using hashes here.

Release history Release notifications | RSS feed

0.7.3

7 files

This release

0.7.2 This release

7 files

0.7.1

2 files

0.7.0

2 files

0.6.1

2 files

0.6.0

2 files

0.5.6

2 files

0.5.5

2 files

0.5.4

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5.0

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page