Skip to content

Tracing zipper - #166

Open
luketpeterson wants to merge 4 commits into
masterfrom
tracing_zipper
Open

luketpeterson wants to merge 4 commits into
masterfrom
tracing_zipper

Conversation

@luketpeterson

Copy link
Copy Markdown
Collaborator

TraceZipper for debugging and tool to render animation from trace

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Bench A/B vs base: done

job log · 2026-10-28 08:57:28 UTC

base fb979b4 → head 10f93e2, 3 round(s), median of each run averaged; negative is faster

bench cases geomean largest gain largest loss >5% faster >5% slower
shakespeare 7 +2.4% -3.5% shakespeare/shakespeare_words_val_count +13.3% shakespeare/shakespeare_sentences_val_count 0 2
cities 5 -0.1% -0.8% cities/cities_val_count_act +0.6% cities/cities_get 0 0
sparse_keys 96 +0.2% -4.6% sparse_meet/400 +8.8% binary_drop_head/100 0 9
binary_keys 77 -2.5% -13.0% binary_descend_until_max_bytes/2000 +4.2% binary_set_val_at_short_replace/3 9 0
superdense_keys 104 -0.1% -8.1% superdense_meet_low_overlap/32000 +21.7% superdense_insert/800 7 6
act_paths 46 -0.0% -10.5% shape_map_to_act_cata/random_long +9.1% shape_map_to_act_cata/dense_narrow 1 2
zipper_head_owned 16 -1.1% -12.6% zipper_head_owned/map_reader_is_val +2.6% zipper_head_owned/borrowed_head_read_creation 2 0
product_zipper 4 -1.0% -2.0% product_zipper/generic_act_act +0.3% product_zipper/generic_pathmap_act 0 0
38 case(s) moved more than 5%
bench case base head change
superdense_keys superdense_insert/800 14.48 µs 17.63 µs +21.7%
superdense_keys superdense_zipper_step_iter/800 7.70 µs 8.93 µs +16.0%
shakespeare shakespeare/shakespeare_sentences_val_count 2.69 ms 3.05 ms +13.3%
binary_keys binary_descend_until_max_bytes/2000 162.03 µs 140.93 µs -13.0%
zipper_head_owned zipper_head_owned/map_reader_is_val 5.02 µs 4.39 µs -12.6%
superdense_keys superdense_zipper_step_iter/400 3.88 µs 4.32 µs +11.1%
act_paths shape_map_to_act_cata/random_long 44.25 ms 39.59 ms -10.5%
superdense_keys superdense_insert/1600 38.95 µs 42.92 µs +10.2%
act_paths shape_map_to_act_cata/dense_narrow 10.06 ms 10.98 ms +9.1%
sparse_keys binary_drop_head/100 7.44 µs 8.09 µs +8.8%
sparse_keys sparse_zipper_cursor/200 2.32 µs 2.52 µs +8.6%
sparse_keys sparse_zipper_cursor/400 5.12 µs 5.54 µs +8.1%
superdense_keys superdense_meet_low_overlap/32000 26.34 µs 24.21 µs -8.1%
binary_keys binary_descend_until_max_bytes/250 13.92 µs 12.84 µs -7.8%
sparse_keys sparse_zipper_cursor/100 1.18 µs 1.27 µs +7.7%
binary_keys binary_k_path_iter/50 1.27 µs 1.18 µs -7.3%
binary_keys binary_k_path_iter/100 2.44 µs 2.26 µs -7.3%
superdense_keys superdense_k_path_iter/16000 107.33 µs 100.40 µs -6.5%
binary_keys binary_set_val_at_short_replace/1 12 ns 12 ns -6.3%
superdense_keys superdense_zipper_cursor/200 1.19 µs 1.11 µs -6.2%
binary_keys binary_drop_head/50 4.58 µs 4.30 µs -6.1%
shakespeare shakespeare/shakespeare_sentences_get 47.37 ms 50.22 ms +6.0%
superdense_keys superdense_k_path_iter/8000 53.36 µs 50.18 µs -6.0%
superdense_keys superdense_zipper_step_iter/3200 23.62 µs 25.02 µs +5.9%
superdense_keys superdense_k_path_iter/4000 26.68 µs 25.11 µs -5.9%
zipper_head_owned zipper_head_owned/owned_head_read_creation 13.07 µs 12.30 µs -5.9%
superdense_keys superdense_k_path_iter/2000 13.34 µs 12.56 µs -5.9%
binary_keys binary_insert/200 16.28 µs 15.34 µs -5.8%
superdense_keys superdense_zipper_step_iter/1600 12.72 µs 13.44 µs +5.6%
binary_keys binary_insert/1600 175.67 µs 166.03 µs -5.5%
superdense_keys superdense_k_path_iter/1000 6.68 µs 6.32 µs -5.4%
sparse_keys sparse_zipper_step_iter/400 63.78 µs 67.22 µs +5.4%
sparse_keys sparse_zipper_step_iter/200 31.39 µs 32.99 µs +5.1%
sparse_keys sparse_zipper_cursor/50 591 ns 621 ns +5.1%
sparse_keys sparse_zipper_step_iter/800 126.97 µs 133.40 µs +5.1%
sparse_keys sparse_zipper_step_iter/100 15.85 µs 16.64 µs +5.0%
act_paths shape_map_to_act_cata/clustered 8.66 ms 9.09 ms +5.0%
binary_keys binary_drop_head/200 19.38 µs 18.41 µs -5.0%

Full tables per bench are in the job log and the bench-out artifact.

@luketpeterson

Copy link
Copy Markdown
Collaborator Author

I'm guessing this is fine to merge because it doesn't interfere with any existing functionality. But I'll keep it as a PR until Matt finalizes the curriculum in case somebody wants to improve or extend the functionality.

@luketpeterson

luketpeterson commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

From @Adam-Vandervorst on discord:

I read through tracing zipper. I'd go differently about this: instead of having struct that mimic the calls with an owned version of the data, have a TeeZipper or whatever that forwards from one zipper (that you're interested in wrapping) and calls a second zipper interface. Now tracing can directly transcribe using the same interface without the duplicate API area

The TraceZipper in the current PR is already a drop-in replacement for another zipper type. So the API the user sees isn't duplicated. As far as separating the enum-based operation format from the API, we still need a format that can represent every operation and its args in a file or buffer, so I don't see how we get away from that.

If I'm understanding the comment correctly, the proposal is for an EchoZipper (I think this is a more appropriate name than TeeZipper because it would need to privilege the primary zipper as the source of truth, and the secondary would just be an observer) and then a TraceZipper or "RecorderZipper" that blindly records the operations but maintains no state.

Is this a correct summary of the proposal?

The 2-object split (EchoZipper + RecorderZipper) comes with some warts we'd need to handle.

  • get_val_mut, get_val_or_set_mut, get_val_or_set_mut_with all return &mut references that the caller can set, and the recorder needs to know what they did after the fact
  • It makes no sense for the RecorderZipper to be a source for anything by itself. So it would record that somebody asks for child_count, or grafted from it, but it couldn't provide anything but null results.

If that's the correct understanding, my question is what is enabled by splitting things into 2 objects?

We'd get an EchoZipper that could have uses outside recording a trace - but it's quite a lot more surface area and I don't see it simplifying anything.

@adamv-symbolica

Copy link
Copy Markdown
Contributor

I'd like to have low-overhead recording for specific sets of methods.
Aside, TraceValue seems to enter a three-way functionality duplication between ACT V serialization and .paths V callback.

@luketpeterson

luketpeterson commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator Author

TraceValue seems to enter a three-way functionality duplication between ACT V serialization and .paths V callback.

Currently ACT only stores u64s and paths doesn't store anything (unless you added something on a private branch. But perhaps this is the time to define a val on-disk format.

I filed #170 I don't want to deal with this now, but it makes sense to handle it eventually.

Making trace format more compact and faster to encode, with bitcode, so now it wouldn't be hideous to use in a production setting although not optimi
zed for perf
Adding multi-target support so zippers can target any implementor of Write for their trace output
@luketpeterson

Copy link
Copy Markdown
Collaborator Author

In 0e83abd I made the trace format a lot more compact and cheaper to write (although I didn't benchmark or optimize the code, I just got rid of obvious slow stuff)

I also added multiple target support so there is no longer a single magic static state holding the output state, in response to Igor's point in the meeting. It still uses thread-locals behind the scenes because that was the most efficient way to preserve the order of multiple zippers on a single thread. While cross-zipper order only technically matters for correctness during operations, it is desirable to keep things in the order they occurred in, where possible.

Lastly I got rid of non () and u64 V types, in anticipation of a better crate-wide solution for serializing Vs. #170

@luketpeterson
luketpeterson requested a review from imlvts October 28, 2026 08:49
@github-actions

Copy link
Copy Markdown

Differential fuzz vs Lean model: success

job log · 2026-10-28 08:51:06 UTC

WARNING: 1 new divergence(s) relative to base

Base was built with its own harness and model (head's did not build against it), so harness changes may show up as differences.

crate: 20000 inputs, seed 7

agree = model and crate match; known = the divergence matches a classified bug in differential.py; new = it matches none. Only the new set is compared input by input below.

side agree known new divergences
head 19613/20000 386 1
base 19678/20000 322 0

1 input(s) diverge on head but not on base; 0 diverge on base but not on head.

Known-class hits that changed

known class base head
AlgebraicStatus::Identity is not returned reliably when nothing changed (finding 8); reproducer in l 122 153
graft_masked_branches() creates the focus when grafting absent branches (finding 15) [graft_child_ma 167 197
which value survives a collision follows node layout rather than the left-biased spec (join/meet and 8 10
an algebraic op keeps a dangling child the spec drops: an empty child node meets/subtracts to itself 2 4
set_val unwraps a None root value 1 0

Newly diverging inputs (head only): 1 input(s), 1 kind(s)

first differs at inputs first example
meet_into 1 random#13813: line 27

Reproducers: first 1 distinct kind(s), shrunk

random#13813: first differs at meet_into, 253 -> 46 bytes

line 27
lean: 27 meet_into ret=None W=0001030201 o0001030201 e0 v- c0 n0 f01 R=_ o_ e1 v- c1 n3 f?
crate: 27 meet_into ret=Identity W=0001030201 o0001030201 e1 v- c1 n0 f01 R=_ o_ e1 v- c1 n3 f?

//! Generated by `pathmap_trace --repro`.  Reproduces a fuzzer input as
//! plain `pathmap` API calls.
//!
//! `wz` writes into map0 and `rz` reads map1; they are separate maps so
//! both can be held at once.  Values are `u64`.

use pathmap::PathMap;
use pathmap::utils::ByteMask;
use pathmap::zipper::*;

fn main() {
    let mut map0 = PathMap::<u64>::new();
    let mut map1 = PathMap::<u64>::new();
    map1.set_val_at(&[], 0);
    map1.set_val_at(&[], 0);
    map1.set_val_at(&[], 0);
    map1.set_val_at(&[2u8, 0], 0);
    map1.set_val_at(&[], 0);
    map1.set_val_at(&[], 0);

    let mut wz = map0.write_zipper_at_path(&[]);
    let mut rz = map1.read_zipper_at_path(&[]);

    /*   0 */ rz.to_next_val();  // to_next_get_val
    /*   1 */ wz.descend_to(&[]);
    /*   2 */ wz.descend_to(&[2u8]);
    /*   3 */ wz.graft_src_at(&rz, &[]);
    /*   4 */ wz.remove_val(false);
    /*   5 */ wz.to_prev_step();
    /*   6 */ rz.reset();
    /*   7 */ wz.meet_into(&rz, false);

    drop(wz);

    // What the maps hold at the end -- compare against the trace's MAP0/MAP1.

    println!("MAP0 {:?}", map0.iter().collect::<Vec<_>>());

    println!("MAP1 {:?}", map1.iter().collect::<Vec<_>>());
}

act: 5000 inputs, seed 7

agree = model and crate match; known = the divergence matches a classified bug in differential.py; new = it matches none. Only the new set is compared input by input below.

side agree known new divergences
head 3570/5000 1430 0
base 3568/5000 1432 0

0 input(s) diverge on head but not on base; 0 diverge on base but not on head.

Known-class hits that changed

known class base head
ACTZipper::val_count() counts from the zipper root, not the focus [act: val_count_ignores_focus] 1431 1430
an algebraic op keeps a dangling child the spec drops: an empty child node meets/subtracts to itself 1 0

@imlvts

imlvts commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

OK. after thinking about this for a while, I think I'm unhappy with the API it's a bit too unwieldy.
I think the entire receiving API should be a function that accepts a slice of events. MaybeVec and returns one back. Maybe a single event, unsure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants