Executive summary

Cleaning until its live pages are about nine tenths full, kladde keeps the live pages of an edited drawing at 1.12 to 1.35 times its live size and of a typed text at 1.10 to 1.19 times; at main’s default settings they take 1.34 to 1.56 times the live size on the drawings, and 1.09 and 1.25 times on the texts. Four cleaning policies ran on kladde-svg’s three drawings, in three layouts, under a drawing editor’s edits that keep to one part of the drawing at a time, and on texts of 64 and 512 KiB, typed into keystroke by keystroke. In the regime this page dwells on, main’s churn floor runs at λ = 0.25, which fills the drawings’ live pages 89 to 92 % full. Cleaning by ripeness, with a static share, and by Bayesian ripeness each aim at a target fill of 0.9, which they reach on the world map and fall short of on the tiger and the coat of arms, at 80 to 90 %. The churn floor at λ = 0.1 fills the drawings’ live pages fullest, 91 to 97 %, at 1.08 to 1.19 times the live size.

Main’s churn floor lies lowest on every workload, or within 0.7 % of the lowest. At equal writes, the ripeness branches keep 0.3 to 15 % more live pages than the churn floor in the drawings’ base and packed layouts, between 0.7 % fewer and 8 % more in the small layout, and 2 to 22 % more on the texts. The churn floor also reaches further: at λ = 0.1 it keeps 1.08 to 1.19 times the drawings’ live size, where the best setting of any ripeness branch stops at 1.10 to 1.26. In the regime, the ripeness branches keep 1 to 12 % more live pages than the churn floor at λ = 0.25, with between 19 % fewer and 10 % more writes.

The ripeness branches give up the cleaning that costs nothing. Main’s free filling takes any victim that fits into the room of the pages a flush writes anyway: at λ = 1, 54 to 919 per run, half to four times as many as the pages it opens to clean, 14 to 1,673. The ripeness branches measured here take only small, ripe victims there, 0 to 116 per run at τ = 0.9, and open 117 to 4,809 pages instead. With main’s free filling instead, the ripeness branches close a third of their gap to the churn floor in the drawings’ base and packed layouts, two thirds in the small layout, and none of it on the texts.

The leaves fill about as much as the data pages under the churn floor, and a few points less under the ripeness branches. In the drawings’ base and packed layouts, the address table’s statements are 19 to 27 % of the live bytes, and the leaves that hold them 20 to 29 % of the live pages. At λ = 0.25, the leaves come out 88 to 90 % full, against data pages 89 to 93 % full, since the churn floor prices a leaf as it prices a data page; under the ripeness branches at τ = 0.9, they come out 79 to 86 % full, against data pages 82 to 93 %. Leaves as full as the data pages would save at most 3.5 % of the live pages. In the drawings’ small layout and on the texts, the leaves are 3 to 7 % of the live pages, and their fill matters little.

Free pages make up much of the smaller files, and cleaning harder adds to them. During the edits, the tiger’s files hold 23 to 47 free pages beside 13 to 34 live ones, and the coat of arms’ 26 to 71 beside 35 to 95; the texts’ free pages are 20 to 30 % of the file at 64 KiB and 11 to 12 % at 512 KiB. Closed, the tiger’s files take 2.4 to 5.1 times its edited SVG, the coat of arms’ 1.5 to 3.0 times, the world map’s 1.07 to 1.9 times, and the texts’ 1.24 to 1.71 times the typed text. From λ = 1 to λ = 0.1, the drawings’ files gain 3 to 41 free pages while they lose 3 to 99 live ones, and the closed files come out between 16 % smaller and 24 % larger.

Filling the pages costs 1.2 to 1.9 times the writes of main’s default on the drawings, and up to 1.2 times on the texts, which is still a fraction of saving the file. In the regime, an edit writes 118 to 432 bytes, against 96 to 233 at main’s default, and a keystroke 40.5 to 54.2, against 36.8 and 49.9. Saving the canonical SVG every 100 edits would write 590 bytes per edit on the tiger, 1,845 on the coat of arms, and 10,699 on the world map, and saving the text every 1000 keystrokes 241 and 700 bytes per keystroke. On the drawings, the journal and the headers take 48 to 83 bytes per edit under every policy, and cleaning decides the rest; on the texts, the journal, the headers, and the leaves that splices fill with restated statements take 31 to 47 bytes of each keystroke’s writes, and the data pages 5 to 13.

Edits and keystrokes stay fast, and flushes short: in the regime, kladde’s calls take 22,000 to 108,000 edits per second on the drawings, against 37,000 to 119,000 at main’s default, and 936,000 to 1,016,000 keystrokes per second on the texts; the median flush takes 3.2 to 10.6 ms on the drawings and 6.8 to 10.8 ms on the texts. On the drawings, the regime costs up to 42 % of the edit rate, the least under single-rate ripeness, up to 17 %; on the world map, the static share’s and the Bayesian posterior’s flushes take 1.2 to 1.7 times as long as single-rate ripeness’s. Reopening an edited file takes 0.7 to 18 ms.

What was measured

kladde-svg’s svg-bench edits an SVG drawing stored in kladde as a drawing editor would, recording a row in kladde-bench’s format every 1000 edits. Its mix: 30 % drags, which set a shape’s translation; 20 % node moves, 8 % node insertions and 7 % node deletions in a path’s data; 15 % recolours; 8 % duplications and 7 % deletions of a shape; and 5 % restacks, which move a shape among its siblings. Nine edits in ten go to a window of a tenth of what they pick from, the shapes, the paths, or the elements without children, at the same place in each, and the window jumps elsewhere every 3000 edits, so that the edits keep to one part of the drawing at a time, as a person’s do. This is svg-bench’s default since kladde-rs e88fd47.

The drawings are the Ghostscript tiger, the coat of arms of Spain, and a blank world map, each edited as many times as it took an earlier, uniform mix to store eight times the drawing’s live size in the base layout:

drawingeditscanonical SVG, before → after the editslive size during the edits: base / packed / small
tiger31,61366,850 → 59,005 bytes92 / 47 / 46 KiB
coat of arms41,130163,768 → 184,541 bytes252 / 131 / 128 KiB
world map177,8311,005,908 → 1,069,891 bytes1,353 / 954 / 943 KiB

Every run makes the same edits, whatever the branch, layout, or setting. The live size is the bytes of the drawing’s allocations, and the canonical SVG what kladde-svg writes, without whitespace between tags and with path data in the grammar’s shortcuts; the edits leave the coat of arms and the world map larger, and the tiger smaller.

The layouts are cargo features of kladde-svg that choose the model’s containers: base stores every list as a vector of fixed slots, packed packs the lists of enums, children, transforms, attributes and path data, and small also keeps short lists and strings inline in their parent. The small layout keeps most of a drawing’s content in a few large allocations, so that its address table is small.

kladde-bench’s text-bench types into one text, stored in a single allocation of the store, keystroke by keystroke at a cursor, as a text editor would, recording a row every 1000 keystrokes; Benchmarks and tests describes it. Of its 100,000 keystrokes, 85 % type a letter, 10 % delete the one before the cursor, 4.5 % move the cursor, nine moves in ten within a window of a tenth of the text that jumps every 5000 keystrokes, and 0.5 % paste 200 letters; every keystroke that changes the text is a splice, which the flush states by restating the statements behind it. It ran on texts of 64 and 512 KiB of random words, which the typing grows to 241,322 and 700,340 bytes; their live size averaged 193 and 642 KiB over the second half.

The store flushes whenever the journal outgrows four pages, Options::journal_budget_pages = 4, and at every row: on the drawings every 230 to 372 edits on average, on the texts every 1000 keystrokes, the journal never filling between rows; otherwise it runs with its defaults.

Four branches of kladde-rs ran, each with the newest size rule:

branchcommitpolicyknob, defaultsettings run
defrag-shifts7eb2ec4main’s churn floorchurn floor λ, 11, 0.5, 0.35, 0.25, 0.15, 0.1
ripeness-svgd3b3a7ecleaning by ripeness, one rate per pagetarget fill τ, 0.750.75, 0.85, 0.9, 0.95
ripeness2-svgf659de7ripeness with a static share per pagetarget fill τ, 0.75as above
bayesian-ripeness-svg65d041aBayesian ripeness, rule (a), with the empirical prior capped at the events it has seentarget fill τ, 0.75as above

The three ripeness branches are ports, onto defrag-shifts, of the branches that Cleaning by ripeness, Ripeness with a static share, and Bayesian ripeness measured on kladde-bench’s workloads. On every branch, a budgeted rewrite of leaves opens no page of its own and costs the budget the pages its restatements take, which the page rewrite estimates at the leaves’ coverage; on main, the churn floor weighs a leaf by the same estimate, as the budget loop has it. text-bench came from kladde-rs 7c5cdd6, on the branch text-bench, and was added to each of these commits with git cherry-pick, without its --churn-floor option on the ripeness branches, whose store lacks it.

Main’s budget controller aims the file at a target fill of 0.8, measured over all of its pages, free ones included. Over the second half of every run but those on the 512 KiB text, it held the budget at its maximum, 256 pages, but for a row or two in four runs on the world map, where it dipped to 196 pages at the lowest, so that the churn floor alone decided what was cleaned; on the world map, whose file starts out fuller than the target, it held the budget lower during parts of the first half. On the 512 KiB text, the file passed the target after about 11,000 keystrokes, the controller had cut the budget to its minimum, one page, by 39,000 to 42,000, and every λ cleaned about alike. The ripeness branches’ controller moves the price of space until the data pages and leaves are as full as the target.

The regime this page dwells on is live pages about nine tenths full: λ = 0.25 for the churn floor, and τ = 0.9 for the ripeness branches, whose results at 0.85, 0.9, and 0.95 hardly differ on the tiger and the coat of arms. Main’s default, λ = 1, is the reference, and λ = 0.1, which fills the pages most, the churn floor’s furthest reach; the sweep has every setting. The tables name the churn floor by its λ, and the three ripeness branches, each at τ = 0.9, by their estimate: ripeness, static share, and Bayesian.

Space is averaged over the second half of each run, and writes are what kladde wrote during that half, per edit or keystroke, as on the other evaluation pages. Live pages are the data pages and the leaves, the latter with the interior pages of the address table, but without the header and the journal. Fill is the live bytes in a page over the 4,081 bytes it can hold: for a data page the bytes that fragments resolve to, for a leaf its statements’ framing and inline payloads. Free pages are pages within the file that hold nothing live; the closed file is the file’s size after the last edit and close, which truncates it after its last live page. Writes count the journal’s bytes and every page written, headers included, at 4,096 bytes each.

The space and the writes come from 198 runs, eight at a time, 162 of svg-bench and 36 of text-bench; the times from two further passes, one run at a time on an otherwise idle host, of the six configurations in the tables on the drawings and of every setting on the texts. All were built with --release by Rust 1.99.0 and ran on an Intel Core i7-1165G7, four cores and eight threads, with 16 GB of memory under Linux 7.0, in a development container, on mains power. The benchmarks are deterministic but for their times, and both timing passes reproduced the first runs’ tables to the byte; the times give their mean, and how far the two passes differed. data/cleaning-edits/ keeps every run’s table, logs/ beside them the end of each run’s output, with the closed file’s size, and timing/ and timing2/ that of the two timing passes, with the texts’ tables, from which their keystroke rates come.

To reproduce a run, build svg-bench on the branch’s commit, with --features packed or --features small for those layouts, and run it on the drawing with its number of edits, or add text-bench and run it on a text of the size:

git cherry-pick --no-commit 7c5cdd6   # text-bench; drop --churn-floor on the ripeness branches
cargo build --release -p kladde-svg --bin svg-bench --features packed
# --target-fill 0.9 instead of --churn-floor 0.25 on the ripeness branches
target/release/svg-bench crates/kladde-svg/corpus/drawings/world-map.svg \
    out/ --edits 177831 --journal-budget 4 --churn-floor 0.25
cargo build --release -p kladde-bench --bin text-bench
target/release/text-bench out/ --size 524288 --journal-budget 4 --churn-floor 0.25

The figures come from the tables in data/; each run’s directory is named <policy>-<knob><value>-<layout>, with text for the layout of the texts’ runs, and tools/plot-evaluation.py takes it labelled <policy>/<knob>/<value>/<layout>:

D=content/evaluation/data/cleaning-edits; F=content/evaluation/figures/cleaning-edits
runs=()
for dir in $D/*-*-*/; do
    IFS=- read -r p k l <<< "$(basename $dir)"; knob=${k%%[0-9]*}
    runs+=("$p/$knob/${k#$knob}/$l=$dir")
done
tools/plot-evaluation.py --only svg-tradeoff,text-tradeoff --out $F "${runs[@]}"
tools/plot-evaluation.py --only svg-pages --out $F $(printf '%s\n' "${runs[@]}" |
    grep -E '^(main/lambda/(1|0.25|0.1)|[a-z0-9]+/tau/0.9)/')

Live pages

On the drawings, the regime takes 9 to 21 % of main’s live pages at its default away, most of them data pages; on the 64 KiB text, 5 to 6 %, while on the 512 KiB text, which main’s default already keeps 95 % full, the churn floor at λ = 0.25 keeps as many and the ripeness branches 7 to 8 % more. Each cell gives the live pages, and in parentheses the leaves among them; the first column gives the live bytes they hold, in data pages and in leaves, which differ by at most 1.4 % between the policies:

workloadlayout or sizelive bytes, KiBλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase72 + 2634.2 (8.8)26.9 (7.1)25.8 (6.9)30.1 (8.2)28.4 (8.2)29.3 (8.0)
tigerpacked37 + 1418.0 (4.6)14.4 (4.0)14.1 (4.0)15.8 (4.4)14.9 (4.4)15.9 (4.4)
tigersmall45 + 116.1 (0.5)13.1 (0.5)12.7 (0.5)14.3 (0.5)13.2 (0.5)14.6 (0.6)
coat of armsbase203 + 6794.8 (23.7)75.2 (18.8)71.0 (17.7)78.2 (20.3)76.6 (20.5)77.1 (20.0)
coat of armspacked107 + 3851.0 (13.6)40.6 (10.8)38.3 (10.1)43.2 (11.9)40.9 (11.5)42.0 (11.0)
coat of armssmall127 + 745.8 (2.1)37.1 (2.0)35.0 (2.0)40.3 (2.6)37.5 (2.8)40.8 (2.8)
world mapbase1,110 + 345478.8 (123.6)398.6 (96.6)379.4 (91.6)408.0 (106.3)409.8 (106.1)409.2 (107.1)
world mappacked834 + 202333.2 (72.6)282.2 (57.0)269.3 (53.8)290.0 (63.3)291.3 (63.5)290.8 (62.6)
world mapsmall937 + 36314.1 (12.0)264.6 (10.6)253.8 (11.1)273.1 (15.1)274.1 (17.1)273.7 (14.3)
text64 KiB192 + 1360.4 (3.8)56.9 (3.8)54.3 (3.7)57.4 (3.9)57.5 (4.1)57.3 (3.8)
text512 KiB640 + 28175.7 (7.6)176.5 (7.6)175.2 (7.6)188.5 (10.7)187.9 (11.3)189.9 (10.6)

Over the live size, which the first column gives here, the same live pages:

workloadlayout or sizelive size, KiBλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase921.491.171.121.311.241.28
tigerpacked471.521.221.191.331.261.35
tigersmall461.391.121.091.231.141.26
coat of armsbase2521.501.191.131.241.221.22
coat of armspacked1311.561.241.171.321.251.29
coat of armssmall1281.431.161.091.261.171.27
world mapbase1,3531.421.181.121.211.211.21
world mappacked9541.401.181.131.221.221.22
world mapsmall9431.341.121.081.161.161.16
text64 KiB1931.251.181.131.191.191.19
text512 KiB6421.091.101.091.171.171.18

The live bytes exceed the live size in the drawings’ base and packed layouts, since the statements that describe the allocations are live too.

The churn floor fills the drawings’ data pages 89 to 93 % full at λ = 0.25 and 93 to 97 % at λ = 0.1; the ripeness branches at τ = 0.9 fill them 82 to 93 % full. Each cell gives the fill of the data pages, of the leaves, and of all live pages, in percent:

workloadlayout or sizeλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase71 / 74 / 7191 / 90 / 9196 / 93 / 9583 / 79 / 8290 / 79 / 8785 / 81 / 84
tigerpacked70 / 75 / 7189 / 88 / 8993 / 86 / 9182 / 80 / 8190 / 79 / 8782 / 80 / 81
tigersmall73 / 28 / 7291 / 33 / 8994 / 35 / 9183 / 28 / 8190 / 29 / 8882 / 32 / 80
coat of armsbase72 / 71 / 7290 / 89 / 9096 / 94 / 9589 / 83 / 8792 / 82 / 8990 / 84 / 88
coat of armspacked72 / 71 / 7290 / 89 / 9096 / 93 / 9586 / 81 / 8592 / 83 / 9088 / 86 / 87
coat of armssmall73 / 80 / 7391 / 81 / 9097 / 85 / 9685 / 64 / 8492 / 65 / 9084 / 63 / 83
world mapbase78 / 70 / 7692 / 89 / 9297 / 95 / 9693 / 83 / 9092 / 83 / 9093 / 82 / 90
world mappacked80 / 70 / 7893 / 89 / 9297 / 95 / 9793 / 81 / 9093 / 80 / 9092 / 81 / 90
world mapsmall78 / 76 / 7893 / 91 / 9297 / 94 / 9791 / 66 / 9092 / 59 / 9091 / 70 / 90
text64 KiB85 / 87 / 8591 / 87 / 9095 / 88 / 9590 / 81 / 9091 / 74 / 8990 / 84 / 90
text512 KiB96 / 93 / 9595 / 93 / 9596 / 94 / 9690 / 65 / 8991 / 61 / 8990 / 65 / 88

On the world map, the ripeness branches reach their target, their live pages 90 % full at τ = 0.9 in every layout; on the tiger and the coat of arms, single-rate ripeness and the Bayesian posterior stop at 80 to 88 %, and the static share at 87 to 90 %. Their controller raises the price of space as long as the pages are emptier than the target, but on the smaller files no price makes it clean the pages that the churn floor cleans, and the targets 0.85, 0.9, and 0.95 end within a few points of each other. Of the three, single-rate ripeness writes least, and on the world map it fills the pages as full as the other two.

What the ripeness branches do not take is free cleaning. Free filling puts victims into the room left in the pages a flush writes anyway, which costs no page of its own. Main takes any whole victim that fits there, sparsest first; the ripeness branches measured here take only ripe victims whose survivors fill at most θ, 5 %, of a page, and then the survivors of a cursor through the previous flush’s least-filled page, so that survivors, cold by selection, do not settle next to fresh content, as the ripeness draft proposed before it took main’s free filling. So the ripeness branches clean nearly everything on pages they open for it. Each cell gives, over a whole run, the data victims that free filling took, the data pages opened only to take victims, and the leaves rewritten, by the budget or by the cut’s fillers:

workloadlayout or sizeλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase112 / 145 / 21966 / 545 / 41178 / 923 / 6017 / 340 / 2925 / 524 / 3106 / 395 / 324
tigerpacked90 / 64 / 11856 / 301 / 20051 / 482 / 2758 / 196 / 1681 / 336 / 1680 / 213 / 175
tigersmall97 / 120 / 6166 / 413 / 6559 / 664 / 638 / 241 / 615 / 415 / 617 / 245 / 64
coat of armsbase215 / 226 / 388131 / 1,045 / 858100 / 2,282 / 1,5699 / 847 / 64511 / 1,155 / 66015 / 1,004 / 737
coat of armspacked123 / 145 / 22390 / 604 / 46084 / 1,186 / 74813 / 433 / 34813 / 701 / 39020 / 495 / 443
coat of armssmall147 / 225 / 20298 / 883 / 24977 / 1,589 / 25511 / 536 / 21910 / 931 / 2296 / 552 / 208
world mapbase919 / 1,194 / 3,002673 / 4,729 / 6,522646 / 10,636 / 12,45172 / 4,405 / 5,11288 / 4,630 / 5,65796 / 4,720 / 5,256
world mappacked795 / 817 / 1,792573 / 3,130 / 3,867523 / 7,691 / 7,99350 / 3,102 / 2,86597 / 3,269 / 3,05060 / 3,173 / 3,018
world mapsmall774 / 1,673 / 2,400571 / 5,996 / 4,157515 / 12,072 / 5,90376 / 4,406 / 2,435116 / 4,809 / 2,38087 / 4,357 / 2,870
text64 KiB54 / 14 / 15750 / 69 / 16145 / 210 / 1628 / 117 / 1278 / 148 / 1194 / 133 / 134
text512 KiB63 / 90 / 17457 / 101 / 19061 / 110 / 1866 / 137 / 229 / 178 / 86 / 146 / 29

At λ = 1, main takes half to four times as many victims free as it opens pages for; the ripeness branches at τ = 0.9 take 0 to 16 % as many free as main does at λ = 1. That count leaves out the pages the cursor empties, a whole statement at a time, in the room that small ripe victims leave. Ripeness with main’s free filling gives the ripeness branches main’s free filling, which takes about one victim per flush on the drawings, as main’s does: they write 2 to 3 % less at the same target, and close a third to two thirds of their gap to the churn floor on the drawings, and none of it on the texts.

Leaves

Under the churn floor, the leaves of the drawings’ base and packed layouts fill about as much as the data pages: 70 to 75 % at λ = 1, 88 to 90 % at λ = 0.25 and 86 to 95 % at λ = 0.1, where the data pages are 70 to 80 %, 89 to 93 % and 93 to 97 % full. The churn floor prices a leaf as it prices a data page: rewriting it reclaims the page less what the leaf’s statements take restated, and writes those, which the page rewrite estimates at the leaf’s coverage, so that a leaf is a victim up to the same fill as a data page. In those layouts, the statements are 19 to 27 % of the live bytes and the leaves 20 to 29 % of the live pages, so their fill matters; leaves as full as the data pages would save at most 2.8 % of the live pages under the churn floor.

Under the ripeness branches, the leaves stay a few points emptier than the data pages: at τ = 0.9, 79 to 86 % full, against 82 to 93 %, and leaves as full as the data pages would save at most 3.5 % of the live pages. They charge a rewrite of leaves to the budget by the same estimate as main, but rank leaves by their ripeness; this page has not traced what keeps them emptier.

Where the leaves hold little, their fill matters little. In the drawings’ small layout, the statements are 1 to 5 % of the live bytes and the leaves 3 to 7 % of the live pages, and on the texts 4 to 6 % and 4 to 7 %. On the 512 KiB text, the ripeness branches’ leaves stay 61 to 65 % full; they rewrite 8 to 29 leaves over a whole run, and this page has not traced why so few.

The trade-off between space and writes

Each policy trades writes for space along its own curve, and the churn floor’s lies lowest on every workload, or within 0.7 % of the lowest. The curves run through each policy’s settings, from the one that cleans least, hollow, to the one that cleans most:

Live pages over live size against the bytes written per keystroke, a panel per text, for the churn floor at λ = 1, 0.5, 0.35, 0.25, 0.15 and 0.1, and the ripeness branches at τ = 0.75, 0.85, 0.9 and 0.95

  • On the texts, the churn floor lies lowest at every setting. On the 64 KiB text, the ripeness branches keep 2 to 22 % more live pages at equal writes. On the 512 KiB text, every λ keeps 1.09 to 1.10 times the live size at 50 to 52 bytes per keystroke, since the controller holds the budget at one page; the ripeness branches keep 7 to 12 % more live pages at those writes, and come within 4 % of it only at τ = 0.95, with 53.5 to 56.3 bytes per keystroke.
  • The ripeness branches’ default target, 0.75, cleans less than main’s default on the texts and the world map, and more on the smaller drawings. It keeps 1.44 and 1.45 times the 64 KiB text’s live size, where main’s default keeps 1.25, and 2 to 5 % more live pages than main’s default on the world map; on the tiger and the coat of arms, it keeps 2 to 6 % fewer, for 8 to 24 % more writes.

Each drawing takes a row, whose panels share their range of live pages:

Live pages over live size against the bytes written per edit, a panel per drawing and layout, for the churn floor at λ = 1, 0.5, 0.35, 0.25, 0.15 and 0.1, and the ripeness branches at τ = 0.75, 0.85, 0.9 and 0.95

  • In the drawings’ base and packed layouts, the ripeness branches keep 0.3 to 15 % more live pages than the churn floor at equal writes, the most at τ = 0.75, and the least at their fuller targets on the world map, where single-rate ripeness at τ = 0.95 comes within 0.3 %.
  • In the small layout, the curves come closest: the ripeness branches keep 1 to 8 % more live pages than the churn floor at equal writes on the tiger and the coat of arms, and on the world map between 0.7 % fewer, single-rate ripeness at τ = 0.95, and 8 % more.
  • On the tiger and the coat of arms, the ripeness branches’ curves bunch once their target passes 0.85: each branch’s settings from 0.85 up lie within 0.07 of each other in live pages over live size, at 1.14 to 1.35 times the live size. On the world map, each step of the target still takes live pages off, down to 1.10 to 1.17 times the live size at τ = 0.95.
  • The churn floor reaches further: at λ = 0.1 it keeps 1.08 to 1.19 times the drawings’ live size, for 168 to 731 bytes per edit, where the best setting of any ripeness branch stops at 1.10 to 1.26.

The equal-write comparisons interpolate the churn floor’s curve between its settings, at every setting of the ripeness branches whose writes lie within it.

Files

Free pages take 14 to 69 % of the drawings’ files during the edits, and on the tiger more pages than the live ones under every policy but main’s default in the base layout; of the texts’ files, they take 11 to 30 %. Each bar is a drawing’s file averaged over the second half of the edits, in multiples of the live size:

What the file holds during the edits, as multiples of the live size: data pages, leaves, free pages, and the headers and the journal, for main's churn floor at λ = 1, 0.25 and 0.1 and the ripeness branches at τ = 0.9

Each cell gives the file’s pages during the edits, and in parentheses the free ones among them:

workloadlayout or sizeλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase64 (27)69 (39)76 (47)67 (34)70 (38)69 (36)
tigerpacked44 (23)46 (29)52 (35)46 (28)51 (33)50 (31)
tigersmall45 (26)44 (28)49 (34)45 (27)47 (31)43 (26)
coat of armsbase127 (29)125 (47)144 (71)125 (44)128 (48)124 (44)
coat of armspacked80 (26)83 (39)94 (53)80 (33)86 (43)84 (39)
coat of armssmall78 (29)79 (39)89 (51)78 (34)83 (43)79 (35)
world mapbase579 (97)510 (109)504 (121)477 (66)487 (74)482 (69)
world mappacked428 (92)369 (83)367 (94)357 (64)371 (76)365 (71)
world mapsmall408 (91)346 (78)355 (98)367 (91)345 (68)354 (77)
text64 KiB79 (16)79 (20)81 (24)81 (21)83 (23)82 (22)
text512 KiB201 (23)203 (24)201 (24)214 (23)216 (26)217 (25)

The rest of a file, about three pages, is the two header pages and the journal.

Free pages are returned only in bulk. Truncation waits until 16 pages at the file’s end are free, Options::truncate_tail, 64 KiB, more than the tiger’s live size in its packed and small layouts, and compaction mode starts only once free pages below the tail exceed a quarter of the file, Options::hole_share. Cleaning harder frees more pages per flush: at λ = 0.1, the drawings’ files hold 3 to 41 free pages more than at λ = 1, and compaction mode ran in 2 to 38 flushes where it ran in 1 to 10; the 64 KiB text’s file holds 8 more, and the 512 KiB text’s, whose budget the controller holds at one page, 1 more. This page has not measured how much of the free space either threshold accounts for.

Closed, the drawings’ files take 1.07 to 5.1 times their edited SVG and the texts’ 1.24 to 1.71 times the typed text, and which policy closes smallest changes from workload to workload. close truncates the file after its last live page, so what it keeps is the free pages below it:

workloadlayout or sizeedited SVG or text, KiBλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase58244228296248248228
tigerpacked58164164172164148148
tigersmall58136164168144156144
coat of armsbase180544472524484488480
coat of armspacked180312312332292272304
coat of armssmall180292276324280300296
world mapbase1,0451,9801,9121,7961,7641,7601,756
world mappacked1,0451,5081,3561,2721,2401,1881,324
world mapsmall1,0451,2801,1201,1961,1601,1561,140
text64 KiB236384360400380404380
text512 KiB684848868876944924932

The closed sizes vary by up to a third between policies that keep nearly as many live pages, since they depend on where the last live page happens to lie when the edits end.

Writes

Filling the pages costs writes: on the drawings, the churn floor at λ = 0.25 writes 1.4 to 1.8 times as much per edit as at λ = 1, and the ripeness branches at τ = 0.9 1.2 to 1.9 times; on the texts, up to 1.2 times. The journal and the headers write the same under every policy; the rest is data pages and leaves, the flush’s own and those cleaning rewrites:

workloadlayout or sizejournal + headersλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase52 + 15152230311185227210
tigerpacked37 + 1196136168119141120
tigersmall37 + 1196135169118146118
coat of armsbase65 + 18204349569312365348
coat of armspacked44 + 12121195289170214187
coat of armssmall45 + 13142213291180228182
world mapbase63 + 17233426731395432421
world mappacked49 + 14164280489261280273
world mapsmall51 + 14215356541300315314
text64 KiB13.2 + 4.136.840.547.942.844.044.3
text512 KiB13.2 + 4.149.951.551.750.454.251.8

All values are bytes per edit on the drawings, and per keystroke on the texts, over the second half of the run. Each cell splits the rest into data pages and leaves:

workloadlayout or sizeλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase56 + 30111 + 51165 + 7981 + 37118 + 4299 + 44
tigerpacked31 + 1660 + 2784 + 3547 + 2369 + 2348 + 23
tigersmall41 + 779 + 8114 + 862 + 791 + 762 + 8
coat of armsbase78 + 43171 + 95311 + 176156 + 74205 + 77181 + 85
coat of armspacked43 + 2391 + 48153 + 8077 + 37114 + 4485 + 45
coat of armssmall63 + 21129 + 27208 + 2697 + 25146 + 25101 + 23
world mapbase80 + 73176 + 169329 + 322185 + 130209 + 143207 + 134
world mappacked58 + 42120 + 97230 + 196127 + 70143 + 73135 + 75
world mapsmall95 + 54196 + 95336 + 139176 + 58193 + 57180 + 68
text64 KiB4.6 + 14.98.0 + 15.215.3 + 15.311.0 + 14.612.6 + 14.112.3 + 14.7
text512 KiB4.8 + 27.84.7 + 29.54.8 + 29.55.7 + 27.49.7 + 27.17.4 + 27.1
  • In the drawings’ base and packed layouts, the leaves take 25 to 49 % of what goes to data pages and leaves, more than their 20 to 29 % of the live pages.
  • On the texts, describing the splices writes more than cleaning does. A keystroke’s splice shifts the text behind the cursor, 133 bytes per keystroke on the 64 KiB text and 529 on the 512 KiB one, which the flush states where it lies, restating the statements that describe it. So the leaves take 14 to 15 and 27 to 30 bytes of each keystroke’s writes, the journal 13, and the headers 4, about the same under every policy, and the data pages, which cleaning writes, 5 to 15. Splices without restating the tail proposes how to state a splice without that.

Every policy writes far less than saving the file would. Saving the canonical SVG every 100 edits would write 590 bytes per edit on the tiger, 1,845 on the coat of arms, and 10,699 on the world map, and every 1000 edits 59, 185, and 1,070; saving the text every 1000 keystrokes would write 241 and 700 bytes per keystroke. In the regime, kladde writes 118 to 230 bytes per edit on the tiger, 170 to 365 on the coat of arms, and 261 to 432 on the world map, and 40.5 to 54.2 bytes per keystroke on the texts.

Times

Edits stay fast and flushes short under every policy; on the drawings, the regime costs up to 42 % of the edit rate, and on the world map the static share and the Bayesian posterior take up to 1.7 times single-rate ripeness’s flush time; on the texts, the policies differ by little. Each cell gives the edits per second in kladde’s calls, in thousands, or on the texts the keystrokes per second, counting the flushes that a full journal starts within an edit; the median time of the flushes at the rows, in milliseconds; and the median time to reopen the closed file, in milliseconds; each the mean of the two timing passes:

workloadlayout or sizeλ = 1λ = 0.25λ = 0.1ripenessstatic shareBayesian
tigerbase86 / 3.0 / 1.374 / 4.2 / 1.268 / 4.8 / 1.184 / 3.4 / 1.279 / 4.1 / 1.273 / 3.6 / 1.2
tigerpacked119 / 2.8 / 1.2105 / 3.8 / 1.290 / 4.2 / 1.4107 / 3.4 / 1.3101 / 4.1 / 1.495 / 3.9 / 1.4
tigersmall111 / 2.6 / 0.8101 / 3.6 / 0.899 / 3.9 / 0.8108 / 3.2 / 0.8100 / 3.3 / 0.9102 / 3.3 / 1.1
coat of armsbase62 / 3.2 / 3.449 / 4.3 / 3.034 / 6.9 / 2.955 / 3.8 / 3.145 / 5.3 / 3.542 / 5.3 / 3.3
coat of armspacked85 / 3.5 / 3.470 / 5.2 / 3.058 / 7.5 / 2.977 / 4.4 / 3.167 / 5.3 / 3.360 / 6.5 / 3.0
coat of armssmall74 / 4.2 / 1.867 / 5.0 / 1.764 / 5.1 / 1.775 / 4.5 / 1.864 / 5.3 / 2.068 / 4.8 / 1.8
world mapbase37 / 4.5 / 17.629 / 6.3 / 15.221 / 9.4 / 14.531 / 6.6 / 16.023 / 8.6 / 17.022 / 10.6 / 16.8
world mappacked45 / 4.3 / 16.335 / 5.8 / 14.826 / 8.6 / 14.039 / 6.0 / 15.432 / 7.3 / 16.727 / 9.4 / 16.0
world mapsmall37 / 6.1 / 8.029 / 7.8 / 7.924 / 10.8 / 7.635 / 6.3 / 8.229 / 8.1 / 8.226 / 10.5 / 8.2
text64 KiB982 / 6.9 / 0.71,016 / 6.8 / 0.7968 / 7.2 / 0.7990 / 6.8 / 0.7994 / 7.3 / 0.9968 / 7.3 / 0.9
text512 KiB958 / 9.2 / 1.4993 / 8.9 / 1.4966 / 9.0 / 1.5936 / 9.1 / 1.7975 / 10.8 / 1.9970 / 9.4 / 2.2
  • Against main’s default, the churn floor at λ = 0.25 edits the drawings 9 to 23 % slower, and at λ = 0.1 11 to 46 %; single-rate ripeness up to 17 %, the static share 8 to 39 %, and the Bayesian posterior 8 to 42 %. The edit rate falls with the writes, since cleaning works inside the flushes that edits start.
  • The flushes at the rows take 2.6 to 6.1 ms at the median on the drawings under main’s default, 3.2 to 10.6 ms in the regime, and 6.8 to 10.8 ms on the texts, where each row’s flush states a thousand keystrokes’ splices, the journal never having filled before it. On the world map, single-rate ripeness flushes as fast as the churn floor at λ = 0.25 in the base and packed layouts, and faster in the small one; the static share and the Bayesian posterior take 1.2 to 1.7 times as long as single-rate ripeness, more than their 5 to 9 % more writes explain, and on kladde-bench too, their ranking cost more.
  • Reopening the edited file takes 0.8 to 1.4 ms on the tiger, 1.7 to 3.5 ms on the coat of arms, 7.6 to 18 ms on the world map, and 0.7 to 2.2 ms on the texts; the layout matters more than the policy, since the small layout’s address table is small.
  • On the texts, every setting takes 936,000 to 1,016,000 keystrokes per second, with median flushes of 6.8 to 7.7 ms at 64 KiB and 8.8 to 10.8 ms at 512 KiB; the settings’ keystroke rates lie within 9 % of each other, little more than identical runs differ.

Identical runs differed by little. The two timing passes differed in the drawings’ edit rate by 1 % at the median and 23 % at most, in their median flush by 3 % and 19 %, and in their reopen times by 2.5 % and 29 %, the largest on the tiger, whose reopens take about a millisecond; on the texts, by 1 to 2 % at the median and at most 8, 10 and 9 %. The reopen times are medians of 21 opens each.

The sweep in numbers

Every setting’s steady state: the live pages over the live size, and the bytes written per edit or keystroke. These are the points of the trade-off figures.

basetigercoat of armsworld map
churn floor, λ = 11.49, 1521.50, 2041.42, 233
churn floor, λ = 0.51.29, 1791.29, 2591.26, 304
churn floor, λ = 0.351.22, 2031.23, 2961.21, 354
churn floor, λ = 0.251.17, 2301.19, 3491.18, 426
churn floor, λ = 0.151.15, 2791.14, 4471.14, 570
churn floor, λ = 0.11.12, 3111.13, 5691.12, 731
ripeness, τ = 0.751.43, 1661.44, 2361.45, 273
ripeness, τ = 0.851.30, 1891.27, 2911.28, 337
ripeness, τ = 0.91.31, 1851.24, 3121.21, 395
ripeness, τ = 0.951.28, 2001.24, 3071.17, 476
static share, τ = 0.751.44, 1721.45, 2521.46, 303
static share, τ = 0.851.26, 2041.27, 3111.28, 369
static share, τ = 0.91.24, 2271.22, 3651.21, 432
static share, τ = 0.951.23, 2261.20, 3781.16, 548
Bayesian, τ = 0.751.43, 1711.44, 2431.45, 285
Bayesian, τ = 0.851.28, 1991.27, 3061.28, 351
Bayesian, τ = 0.91.28, 2101.22, 3481.21, 421
Bayesian, τ = 0.951.28, 1971.22, 3451.16, 549
packedtigercoat of armsworld map
churn floor, λ = 11.52, 961.56, 1211.40, 164
churn floor, λ = 0.51.33, 1091.34, 1501.26, 208
churn floor, λ = 0.351.27, 1261.29, 1641.22, 240
churn floor, λ = 0.251.22, 1361.24, 1951.18, 280
churn floor, λ = 0.151.21, 1551.20, 2471.15, 365
churn floor, λ = 0.11.19, 1681.17, 2891.13, 489
ripeness, τ = 0.751.48, 1081.50, 1381.46, 188
ripeness, τ = 0.851.34, 1211.32, 1711.29, 224
ripeness, τ = 0.91.33, 1191.32, 1701.22, 261
ripeness, τ = 0.951.35, 1171.32, 1761.17, 328
static share, τ = 0.751.46, 1131.50, 1491.47, 202
static share, τ = 0.851.30, 1351.32, 1791.30, 242
static share, τ = 0.91.26, 1411.25, 2141.22, 280
static share, τ = 0.951.26, 1401.26, 2101.16, 365
Bayesian, τ = 0.751.46, 1071.50, 1451.47, 197
Bayesian, τ = 0.851.34, 1201.32, 1741.29, 234
Bayesian, τ = 0.91.35, 1201.29, 1871.22, 273
Bayesian, τ = 0.951.32, 1211.28, 1921.16, 364
smalltigercoat of armsworld map
churn floor, λ = 11.39, 961.43, 1421.34, 215
churn floor, λ = 0.51.20, 1171.26, 1701.20, 269
churn floor, λ = 0.351.17, 1221.20, 1891.16, 307
churn floor, λ = 0.251.12, 1351.16, 2131.12, 356
churn floor, λ = 0.151.11, 1551.11, 2551.09, 451
churn floor, λ = 0.11.09, 1691.09, 2911.08, 541
ripeness, τ = 0.751.31, 1061.40, 1541.39, 217
ripeness, τ = 0.851.22, 1161.27, 1791.23, 256
ripeness, τ = 0.91.23, 1181.26, 1801.16, 300
ripeness, τ = 0.951.23, 1171.26, 1791.13, 334
static share, τ = 0.751.31, 1141.40, 1641.40, 233
static share, τ = 0.851.17, 1311.24, 1941.23, 271
static share, τ = 0.91.14, 1461.17, 2281.16, 315
static share, τ = 0.951.15, 1451.16, 2271.10, 416
Bayesian, τ = 0.751.34, 1091.40, 1561.40, 234
Bayesian, τ = 0.851.28, 1201.26, 1811.23, 274
Bayesian, τ = 0.91.26, 1181.27, 1821.16, 314
Bayesian, τ = 0.951.27, 1181.27, 1821.12, 368
text64 KiB512 KiB
churn floor, λ = 11.25, 36.81.09, 49.9
churn floor, λ = 0.51.23, 37.81.09, 50.4
churn floor, λ = 0.351.20, 39.01.09, 50.8
churn floor, λ = 0.251.18, 40.51.10, 51.5
churn floor, λ = 0.151.15, 44.01.09, 51.6
churn floor, λ = 0.11.13, 47.91.09, 51.7
ripeness, τ = 0.751.45, 37.91.21, 49.5
ripeness, τ = 0.851.26, 40.21.19, 49.9
ripeness, τ = 0.91.19, 42.81.17, 50.4
ripeness, τ = 0.951.17, 43.71.13, 53.5
static share, τ = 0.751.44, 40.51.20, 51.8
static share, τ = 0.851.26, 42.31.19, 52.6
static share, τ = 0.91.19, 44.01.17, 54.2
static share, τ = 0.951.15, 48.71.12, 56.3
Bayesian, τ = 0.751.45, 39.31.22, 51.0
Bayesian, τ = 0.851.27, 41.21.21, 51.1
Bayesian, τ = 0.91.19, 44.31.18, 51.8
Bayesian, τ = 0.951.19, 44.01.12, 55.0