Skip to content

Annotated Sequence Diagrams with Complexity Analysis

This document contains sequence diagrams showing the data flow through PySuricata's streaming algorithms, annotated with time and space complexity for each operation.

Pandas Data Processing Flow

sequenceDiagram
    participant User
    participant Profile
    participant Adapter as PandasAdapter
    participant Accumulator as NumericAccumulator
    participant KMV
    participant Reservoir as ReservoirSampler
    participant Extreme as ExtremeTracker
    participant MG as MisraGries

    User->>Profile: profile(data, config)
    Note over Profile: O(1) - Configuration setup

    Profile->>Adapter: process_chunk(chunk)
    Note over Adapter: O(n) - Iterate through rows

    loop For each numeric column
        Adapter->>Accumulator: update(values)
        Note over Accumulator: O(n) - Process n values

        Accumulator->>KMV: add(value)
        Note over KMV: O(log k) - Binary search insert<br/>Space: O(k) bounded

        Accumulator->>Reservoir: add(value)
        Note over Reservoir: O(1) - Constant time<br/>Space: O(s) bounded

        Accumulator->>Extreme: update(values, indices)
        Note over Extreme: O(n log k) - Process n values<br/>Space: O(k) bounded

        Accumulator->>MG: add(value)
        Note over MG: O(1) - Constant time<br/>Space: O(k) bounded
    end

    Accumulator->>Profile: finalize()
    Note over Accumulator: O(k log k) - Extract results<br/>Space: O(k) bounded

    Profile->>User: Report
    Note over Profile: O(1) - Return results

Polars Data Processing Flow

sequenceDiagram
    participant User
    participant Profile
    participant Adapter as PolarsAdapter
    participant Accumulator as NumericAccumulator
    participant KMV
    participant Reservoir as ReservoirSampler
    participant Extreme as ExtremeTracker
    participant MG as MisraGries

    User->>Profile: profile(data, config)
    Note over Profile: O(1) - Configuration setup

    Profile->>Adapter: process_chunk(chunk)
    Note over Adapter: O(n) - Iterate through rows

    loop For each numeric column
        Adapter->>Accumulator: update(values)
        Note over Accumulator: O(n) - Process n values

        Accumulator->>KMV: add(value)
        Note over KMV: O(log k) - Binary search insert<br/>Space: O(k) bounded

        Accumulator->>Reservoir: add(value)
        Note over Reservoir: O(1) - Constant time<br/>Space: O(s) bounded

        Accumulator->>Extreme: update(values, indices)
        Note over Extreme: O(n log k) - Process n values<br/>Space: O(k) bounded

        Accumulator->>MG: add(value)
        Note over MG: O(1) - Constant time<br/>Space: O(k) bounded
    end

    Accumulator->>Profile: finalize()
    Note over Accumulator: O(k log k) - Extract results<br/>Space: O(k) bounded

    Profile->>User: Report
    Note over Profile: O(1) - Return results

KMV Sketch Memory Optimization

sequenceDiagram
    participant KMV
    participant ExactCounter as _exact_counter
    participant Values as _values
    participant Hash as _u64

    Note over KMV: Before Fix: O(n) memory growth<br/>After Fix: O(k) bounded memory

    KMV->>ExactCounter: add(value)
    Note over ExactCounter: O(1) - Dict lookup<br/>Space: O(min(n, max_exact_tracking))

    alt Exact mode (count < max_exact_tracking)
        ExactCounter->>ExactCounter: increment counter
        Note over ExactCounter: O(1) - Dict update
    else Transition to approximation mode
        ExactCounter->>Values: convert to hashes
        Note over Values: O(k) - Convert exact values<br/>Space: O(k) bounded

        ExactCounter->>ExactCounter: clear()
        Note over ExactCounter: O(1) - Free memory

        Values->>Values: sort()
        Note over Values: O(k log k) - Sort hashes
    end

    alt Approximation mode
        KMV->>Hash: _u64(value)
        Note over Hash: O(1) - Hash computation

        Hash->>Values: insert if smaller
        Note over Values: O(log k) - Binary search insert<br/>Space: O(k) bounded
    end

    KMV->>Values: estimate()
    Note over Values: O(1) - Direct calculation<br/>Space: O(k) bounded

ExtremeTracker Memory Optimization

sequenceDiagram
    participant Extreme as ExtremeTracker
    participant MinHeap as _min_heap
    participant MaxHeap as _max_heap
    participant Heapq

    Note over Extreme: Before Fix: O(k × chunks) temporary growth<br/>After Fix: O(k) constant space

    Extreme->>Extreme: update(values, indices)
    Note over Extreme: O(n log k) - Process n values

    loop For each value
        Extreme->>MinHeap: _add_to_min_heap(index, value)
        Note over MinHeap: O(log k) - Heap insert<br/>Space: O(k) bounded

        Extreme->>MaxHeap: _add_to_max_heap(index, value)
        Note over MaxHeap: O(log k) - Heap insert<br/>Space: O(k) bounded
    end

    alt Min heap not full
        MinHeap->>Heapq: heappush(value, index)
        Note over Heapq: O(log k) - Heap insert
    else Min heap full
        MinHeap->>MinHeap: find largest value
        Note over MinHeap: O(k) - Linear search

        alt New value smaller
            MinHeap->>MinHeap: replace largest
            Note over MinHeap: O(k) - Find and replace
            MinHeap->>Heapq: heapify()
            Note over Heapq: O(k) - Restore heap property
        end
    end

    alt Max heap not full
        MaxHeap->>Heapq: heappush(-value, index)
        Note over Heapq: O(log k) - Heap insert (negated)
    else Max heap full
        MaxHeap->>MaxHeap: find smallest original value
        Note over MaxHeap: O(k) - Linear search

        alt New value larger
            MaxHeap->>MaxHeap: replace smallest
            Note over MaxHeap: O(k) - Find and replace
            MaxHeap->>Heapq: heapify()
            Note over Heapq: O(k) - Restore heap property
        end
    end

    Extreme->>Extreme: get_extremes()
    Note over Extreme: O(k log k) - Extract and sort<br/>Space: O(k) bounded

Chunk Metadata Optimization

sequenceDiagram
    participant Accumulator as NumericAccumulator
    participant Config as NumericConfig
    participant Boundaries as _chunk_boundaries
    participant Missing as _chunk_missing
    participant Counter as _chunk_count

    Note over Accumulator: Before Fix: O(num_chunks) unbounded growth<br/>After Fix: O(min(num_chunks, max_chunks)) bounded

    Accumulator->>Config: check enable_chunk_metadata
    Note over Config: O(1) - Configuration check

    alt Chunk metadata enabled
        Accumulator->>Counter: check _chunk_count < max_chunks
        Note over Counter: O(1) - Counter check

        alt Under limit
            Accumulator->>Boundaries: append(cumulative_rows)
            Note over Boundaries: O(1) - List append<br/>Space: O(chunk_count)

            Accumulator->>Missing: append(missing_count)
            Note over Missing: O(1) - List append<br/>Space: O(chunk_count)

            Accumulator->>Counter: increment()
            Note over Counter: O(1) - Counter increment
        else Over limit
            Accumulator->>Config: disable chunk metadata
            Note over Config: O(1) - Switch to summary mode

            Accumulator->>Boundaries: stop tracking
            Note over Boundaries: O(1) - Stop appending<br/>Space: O(max_chunks) bounded
        end
    else Chunk metadata disabled
        Accumulator->>Accumulator: skip tracking
        Note over Accumulator: O(1) - No memory usage<br/>Space: O(1) constant
    end

    Accumulator->>Accumulator: finalize()
    Note over Accumulator: O(chunk_count) - Process metadata<br/>Space: O(min(chunk_count, max_chunks)) bounded

Complexity Summary

Time

  • Per element: O(1) for the moments and the sketch updates; O(log k) where a heap is involved
  • Per chunk: O(n) in the chunk size
  • Total: O(N) in the dataset — a single pass, each row read once

Space

Every bound below is per column:

structure space set by
streaming moments O(1)
reservoir sample O(s) numeric_sample_size (20,000)
KMV sketch O(k) max_uniques (2,048)
Misra-Gries table O(k) top_k (50)
extreme tracker O(k)
chunk metadata O(min(chunks, max_chunks))

Total: O(cols × (s + k)). Nothing in that expression is the row count, which is the whole claim — memory is flat in rows.

It is not flat in columns. Multiplying by cols is not a rounding error: at roughly 529 KB per column, a 20,000 × 600 frame peaks at 631 MB against 344 MB for a 1,000,000 × 14 one on more cells. That is a known limit, tracked in #207, and it is the honest reading of the table above.

Measure it rather than taking a figure from this page: python -m benchmarks.kernels prints the per-kernel memory roofline, and tests/test_memory_stress.py asserts the row-axis flatness in subprocesses on every run.