GTFS-Madrid

Question: when one row of Madrid’s transit feed changes, how many values does .me recompute, and does that grow with the city?

Answer (measured, this.me 4.2.1): no. A write recomputed k = 1 value at the median, 18 at p95 and 27 at most, at scale 1 (2,364 stop_times) and at scale 10 (23,640); the scale-100 run on this.me 4.1.0 (236,400) gave the same 1 / 18 / 27. What grows with the data is a delete: 8.9 ms p50 at scale 1, 119 ms at scale 10.

The feed is GTFS-Madrid-Bench, changed with the benchmark’s own update packages. k is explain(probe).meta.k (me.explain(path)), the number of derived values one write recomputes; Extreme Fan-Out explains the wave it counts.

Live: AI decides, .me computes → runs on the same scale-1 / scale-10 feed in your browser, also on this.me 4.2.1. Its k is much larger, and that is expected: there one write changes policy.factor, a value every headway reads, so the wave is the whole headway subtree (every window, trip and route), the fan-out case of Extreme Fan-Out. Here each write is one feed row, so the wave is that row’s foreign-key neighbourhood. Its timings come from a browser Web Worker, these from Node.

Run: this.me 4.2.1 from npm (dist/me.es.js sha256 0bd2cc60d15dfc9ff8942d390c075bbc22b4ae3a24d3cbfe581511ea84478757), Node v22.23.3, shared KVM VM (Intel Xeon, 8 vCPU, 16 GiB), Linux 6.12, 2026-10-13 (UTC). History rows: this.me 4.2.0 (sha256 8cc94d5273b05728713e7c06bcba6ab0d88c85d2a748dc885e7bca745605a61a, same VM, 2026-10-10) and this.me 4.1.0. Every results file records its own run line.

Data and workload

The Van Assche et al. CHANGE packages (Zenodo 10.5281/zenodo.14038823): GTFS-Scale-1-CHANGE.tar.xz, GTFS-Scale-10-CHANGE.tar.xz, GTFS-Scale-100-CHANGE.tar.xz, generated with VIG from GTFS-Madrid-Bench (sizes and what to extract: Madrid GTFS dataset). Each run loads base, then applies every CREATE / UPDATE / DELETE from seed0 and measures each write. Write time is the write itself, including its propagation wave.

How GTFS maps onto .me

Rows become paths (gtfs.trips.T1.serviceId, written as me.gtfs.trips.T1.serviceId(value), see me.whatever(what)) and foreign keys become dependencies:

calendar.txtdays, start/end datesservices.S.active= days && datestrips.T.live= services.S.activeroutes.txtroute rowroutes.R.activeDELETE → absenttrips.T.validRoute= routes.R.activetrips.txtroute_id, service_idtrips.T.routeIdroute codestopTimes.*.routeId= trips.T.routeIdstop_times.txtone row per stopidx…Member.<slot>0 → 1 on CREATEtripCount, stopTimeCount= sum of slots
Green: feed files. Blue: derived values a feed write reaches. Amber: CREATE membership slots and the counts that sum them. A write recomputes only what is downstream of it.

k counts the derived values reached from the written row through those edges. This is not an RML/RDF materializer: it measures how a change propagates after the graph exists, not how a knowledge graph is built.

Setup

Node ≥ 22.6 (it runs the .ts files directly). All commands run from Typescript/. Full details in the GTFS-Madrid README.

git clone https://github.com/neurons-me/.me.git && cd .me/Typescript
npm install
npm pack this.me@4.2.1 && tar xzf this.me-4.2.1.tgz
rm -rf dist && mv package/dist dist
sha256sum dist/me.es.js

sha256sum should print 0bd2cc60…8478757 (full value above). Put the Zenodo CHANGE CSVs in tests/Benchmarks/GTFS-Madrid/data/scale{1,10}/{base,seed0}/. The scale-10 sweep takes about 1 minute, the skew sweep about 16 minutes.

1. How many values a write touches as the data grows

0102030scale 12,364 stop_timesscale 1023,640 stop_timesscale 100236,400 stop_times · 4.1.0k max 27k p95 18k p50 1
k per write (p50, p95, max) at three VIG scales. Filled: this.me 4.2.1. Hollow: scale 100, this.me 4.1.0 (not re-run). The data grows 100×; the lines stay flat.
kernel scale stop_times writes k p50 / p95 / max write p50 / p95 (ms) CREATE p50 DELETE p50 load (ms)
4.2.1 1 2,364 151 1 / 18.5 / 27 0.23 / 9.4 0.20 8.9 494
4.2.1 10 23,640 1,304 1 / 18 / 27 0.21 / 122 0.20 119 4,244
4.2.0 (history) 1 2,364 151 1 / 18.5 / 27 0.29 / 9.4 0.24 9.2 476
4.2.0 (history) 10 23,640 1,304 1 / 18 / 27 0.22 / 165 0.21 144 4,730
4.1.0 (history) 100 236,400 13,401 1 / 18 / 27 not re-run — — —

k is identical on 4.1.0, 4.2.0 and 4.2.1 for every write. The write p95 at scale 10 is the deletes (run 3). Scale 100 was not re-run on 4.2.1: the archive is 1.2 GB and, with each delete scanning the store (run 3), the sweep’s roughly 2,400 deletes would take on the order of an hour on this VM. It stays as 4.1.0 history.

ME_VERSION=4.2.1 node tests/Benchmarks/GTFS-Madrid/sweep-seed0.ts --scale=1
ME_VERSION=4.2.1 node tests/Benchmarks/GTFS-Madrid/sweep-seed0.ts --scale=10

2. What a write costs when one parent is huge

VIG grows the data uniformly (every trip keeps about 18 stop_times), while real feeds are uneven. So the skew run reassigns trips.service_id and stop_times.trip_id with Zipf (a = 0 … 2) and hub profiles at scale 10, same row counts, 3 seeds each (22 runs), and replays the same workload plus a census of the widest parents.

0.11101001000 ms1101001,00010,000k = values the write recomputes (log)DELETE ≈ 111 ms + 49 µs·kUPDATE ≈ 0.045 ms + 35 µs·k
Median write time by k bucket, all 22 runs pooled, this.me 4.2.1. An UPDATE costs in proportion to k. A DELETE starts at about 110 ms whatever k is (it scans the store, run 3) and only then grows with k.
profile k max (widest parent) UPDATE p50 (ms) DELETE p50 (ms) rebuild (ms) rebuild ÷ UPDATE
VIG 27 0.45 (4.2.0: 0.53) 117 (140) 3,604 (5,784) 7,932× (10,932×)
Zipf 1 3,051 0.30 (0.48) 112 (140) 3,654 (5,163) 12,059× (10,849×)
Zipf 2 14,378 0.14 (0.20) 117 (146) 3,875 (4,974) 27,942× (24,578×)
hub 50% 11,820 0.23 (0.32) 117 (137) 3,789 (4,759) 16,220× (14,713×)

All values this.me 4.2.1; in brackets this.me 4.2.0. Zipf 1 / 2: exponent a = 1 / 2; hub 50%: one service and one trip own half of their children. k is identical on both kernels.

node tests/Benchmarks/GTFS-Madrid/skew/generate-skew.ts --scale=10
ME_VERSION=4.2.1 node --expose-gc tests/Benchmarks/GTFS-Madrid/skew/sweep-skew.ts --scale=10
node tests/Benchmarks/GTFS-Madrid/skew/report-skew.ts --scale=10

3. What a delete costs (known weakness)

UPDATE cost follows k; DELETE cost does not. Each delete, me["-"], scans the store: about 110–117 ms p50 at scale 10 whatever k is (run 2’s table: the same from k max 27 to 14,378), against 8.9 ms at scale 1. So a delete’s cost depends on n, the size of the data, not on k. On top of that it adds about 49 µs per k (4.2.0: 67 µs). Worst measured on 4.2.1: 842 ms to delete the 14,378-stop_time hub trip (4.2.0: 1.2 s), still under the rebuild.

Not fixed. The scan is still there on 4.2.1; deletes are about 20% faster than on 4.2.0 because each recomputed formula is cheaper, not because the scan changed. The likely cause: since 4.1.0, removing a subtree scans every subscribed path (Object.keys(refSubscribers)) to find what read the removed paths, and the same removal also walks every derivation and index key. Each of those is a full pass over the store, once per delete.

Deletes are a phase of the same skew run, no separate command.

4. The [] aggregate as the count

this.me 4.2.0 added collection aggregates (stopTimeMember[].on). Used for the counts on this.me 4.2.1, k is the same, but every CREATE re-scans the collection: 3.3 ms p50 at scale 1 and 42 ms at scale 10 (4.2.0: 3.5 and 50), instead of 0.20 ms with explicit sums, and loading scale 10 takes 63 s (4.2.0: 71 s) instead of 4.2 s. So it does not fix the giant-parent CREATE (run 2) yet, and the benchmark keeps explicit sums.

GTFS_AGG=bracket ME_VERSION=4.2.1 node tests/Benchmarks/GTFS-Madrid/sweep-seed0.ts --scale=1
GTFS_AGG=bracket ME_VERSION=4.2.1 node tests/Benchmarks/GTFS-Madrid/sweep-seed0.ts --scale=10

Across versions

The interactive chart needs JavaScript. The same numbers, each with the results file it comes from: madrid-versions.json.

version what changed (CHANGELOG) UPDATE per k trip DELETE p50 (scale 10) scale-10 load [] CREATE p50
4.1.0 Correctness release for derived values: topological wave, early cutoff, removals, cycles 58 µs (2026-10-07) 171 ms 4,414 ms not measured (no [] yet)
4.2.0 Adds the [] collection aggregate; formulas through pointers recompute (#4); root _ stealth for guests 52 µs 144 ms 4,730 ms 50 ms
4.2.1 Performance only, no behavior change: each formula is compiled once per kernel instead of on every recompute 35 µs 119 ms 4,244 ms 42 ms

One run per version on the same shared VM, on different days (4.1.0 skew 2026-10-07; 4.1.0 and 4.2.0 sweeps 2026-10-10; 4.2.1 2026-10-13): differences under ~10% (the load column) are within noise. k is the same on all three. Changelog: CHANGELOG.md.

Known limits

More