Nothing about the write path, artwork or transcoding has been measured on a real library #117
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
report.md's numbers are one request set, one library, one host, and read-only: the 56,337-item library was copied without its media files, so playback and transcoding were out of scope for the whole run, and the request set names no specific row.What that leaves unmeasured, in the order it would matter to a running server:
report.mdnotes our responses are 5% larger per request than upstream's, unexplained.Also still open from the same run: ten requests are dropped before anything is timed, because the two servers disagree about them. Most of the six bugs behind them are closed (#86 and its children); the drop set should be re-derived rather than assumed to be empty.
Where to start
report.mdis one request set, one library, one host, and read-only — the56,337-item library was copied without its media files, so playback and
transcoding were out of scope for the entire run, and the default set names no
specific row.
In the order it would matter to a running server:
Needs media present, and needs #94 first: measuring software-only transcoding
against an upstream that can use a GPU measures the wrong thing.
while reads are served. That is the WAL contention case a read-only run
cannot reach, and the pragmas were chosen for it (M4).
responses run 5% larger per request than upstream's, unexplained.
crates/jellyfin-bench/already gates on agreement before timing anything, soeach of these needs a request set and a library arrangement more than it needs
new machinery.
docs/benchmarks.mdis the setup.How to know it is done
Each of the four has a number in
report.mdwith the same method the read-pathnumbers have — five repetitions, alternating, medians and spread, each server
pinned to its own cores. Also re-derive the ten requests the two servers still
disagree about: most of the six bugs behind them are closed (#86 and its
children), and the drop set should be measured again rather than assumed empty.
House rules
CLAUDE.mdis the short version and these are the parts that bite: tests rununder
cargo nextest; upstream's source is the checkout inupstream/at thepin (
v12.0-rc5,6d501ba4) and not the clone beside the repository;generated files are regenerated rather than edited; a comment says what the code
does, not how it got there; and
Dnreferences stay out of code and comments.ARCHITECTURE.mddecides which crate a thing goes in and which dependency edgesare illegal,
DECISIONS.mdsays why a rule exists.The reference pair is
cargo xtask reference up(andreference provision,which is idempotent); a run is
cargo run -p jellyfin-harness -- replay --upstream URL --ours URL --scenario FILE --out DIR,with
diff-dbfor the two databases.crates/jellyfin-harness/README.mdsayswhat a diff means and what an ignore entry has to carry — every entry needs a
reason and an expiry milestone, and an adds-only history is the smell the risk
table names.
Partly done in
14c4ec6andcfb6e1e. Leaving this open — two of the four areashave numbers, two do not, and one of the two that do turned out to be blocked on
something else.
What was in the way of all of it
A snapshot's rows name the container's paths —
/fixtures/mediaand/config.Upstream resolves a stored path literally, so on a native run it was answering
404to every picture and finding no file behind any item, and the gatedropped the lot. That is almost certainly part of the ten dropped requests this
issue asks about.
A run now moves both prefixes in each copy of the database, and in the library
shortcuts and collection files beside it — upstream reads a folder's physical
locations off those and rescans when they disagree with the rows. It points them
at the library
reference uplaid underreference/var/mediaand lays none ofits own: this host's snapshots were cut against a library laid on the 16th and
the image now produces files stamped the 20th, which is the reconciliation
export_mediaexists to refuse. An anchor is left alone; its paths belong tothe host that wrote it.
Artwork — measured
artwork.json,enriched, three repetitions, ten resize shapes the bundle asksfor. Upstream serves them from
cache/images/resized-images, which it writes onthe first request; this server re-encodes on every request.
~53 responses a second whatever the concurrency. And the 5% larger responses
are gone: bytes per response are 0.98–0.99 of upstream's, so the cost is the
re-encode alone.
Found on the way: the ignore list could not reach an indexed image —
/Items/*/Images/*is a segment short of/Items/{id}/Images/Backdrop/0, and a*matches exactly one segment — so two resizes were being dropped for thereason the entry beside them exists to allow.
Transcoding — runs, cannot be timed
transcoding.jsongets both servers transcoding natively. The gate refuses totime any of it, and that is the result:
stream.mp4h264 maxWidth=320 vb=200000stream.mp4hevc vb=200000stream.tsh264 vb=200000stream.mp4vcodec=copy acodec=mp3One ffmpeg, one host, 20–50% more bytes. Two argument strings that agreed would
not do that, so the throughput number waits on the arguments rather than on the
rig — which is #141's territory, and a good example of the branch a corpus does
not defend. Upstream's command lines are in the run logs; ours logs
transcode startedwithout them, so the two cannot be diffed from a run today.Also uncovered, and not this issue's:
/Audio/{id}/universal500s on a flacsource where upstream answers, in 1–3 ms, so before ffmpeg runs. Both flac
tracks in
enriched,container=mp3andcontainer=aacalike.Not done
playstatealready covers playback reports and user-datawrites. What is missing is a scan running while reads are served, and that is
a second thing running during a timed workload rather than another request
set.
set.
One note on the acceptance: it says each of the four should get a number in
report.mdwith the read-path method. There is noreport.mdanywhere in thetree and nothing references one — the numbers it quotes come from a run whose
write-up is not committed. The two above went into
docs/benchmarks.mdinstead.Still open, and not for want of trying — recording where it stands so the next
attempt does not re-derive this.
The transcoding number is still gated on #94. Neither fixture can produce
it: the anchor is a database and a 7.5 GB metadata directory copied without the
media files, and
reference/'s library is nineteen items of five seconds each."How many concurrent sessions this server sustains" needs media and hardware
parity, and measuring software-only encoding against an upstream that can reach
a GPU measures the wrong thing.
The other three are unblocked and each needs a request set rather than
machinery.
cargo xtask bench --fixture ~/.cache/anchoris the run;docs/benchmarks.mdis the method andcrates/ephyra-bench/README.mdis whatthe generator does. What is missing is the load: playback reports and user-data
writes against a scan for the write path, an image set for artwork, and a scan
run beside upstream's.
The drop set has not been re-derived. One data point that bears on it, from
a run today rather than from the report:
refusals.jsonover a live pairanswered 156 responses with 2 differences, and both are one request —
GET /MusicGenres/InstantMixnaming no id, which both servers refuse with400and which carries a JSON body upstream and a plain-text one here. That isa much smaller disagreement than the ten the report dropped, but it is one
scenario rather than the bench's request set, so it does not settle the number.
The baseline moved today.
#148rebuilt the reference snapshots, and thelibrary on the host was re-laid from the current image with it — so every
figure in
report.mddescribes a fixture that is no longer on disk. A re-runis needed before anything in it is compared against, whatever else changes.
The drop set is re-derived, and it is twenty rather than ten.
cargo xtask bench --fixture ~/.cache/anchor/upstream --user adminover thecommitted workloads, against a native upstream from
reference publish. Thegate runs before anything is timed, so this needed no new request set:
browsingbundleitemsportablehandshake,login,login-0,playbackThree of
browsing's are the onesdocs/benchmarks.mdalready says drop onevery native run — upstream derives the root folder's
Pathfrom where it wasstarted. The rest are not, and they fall into six kinds:
ParentBackdropImageTags: upstream has 1 entry, ours 2. Inbrowsing,itemsandportable, on every row of the listing. The widest of them./Personsputs[hoax record awaiting deletion]at index 7 upstream andA Silver Mt. Zionthere; the same swap shows in
/Itemssorted bySortName. A collationquestion about how a leading
[ranks against a letter.ImageBlurHashesdisagree — we carry a hash keyed by an image upstreamdoes not carry one for, and miss one it does.
/Trailersanswers500upstream and200here, twice inportable.@length: upstream 34549064, ours 35019928on the big
/Itemslistings — five ofitems' seven drops are only this.Related to the report's unexplained 5% on artwork, and now measured on rows
rather than pictures.
/System/Info/Storagereports no libraries upstream and three here,which is the native run moving upstream's data directory and probably belongs
with the three
browsingdrops rather than with the rest.Each is a difference on a 56k-item library that no scenario over the generated
fixture can reach — which is the argument this issue makes in the first place.
They want their own issues rather than a line here; I have not filed them.
The four numbers this issue is actually about are still not measured, and the
transcoding one is still gated on #94.
I tried to measure the artwork path and it cannot be measured with what is
committed. That is a result, so here it is.
cargo xtask bench --fixture ~/.cache/anchor/upstream --workload artwork --reps 5 --duration 10 --warmup 60runs to completion and prints numbers —ours at 37,000 rps against upstream's 3,500, better than tenfold at every
concurrency. Every one of those numbers is worthless. The gate says why:
artwork.jsonnames five item ids, and they are the reference fixture's. On theanchor they name nothing, so the run timed 404 routing rather than image
encoding — exactly the fabricated result
docs/benchmarks.mdwarns about, andthe gate caught it rather than the numbers being believed.
The same holds for the write path:
playstate.jsonnames eleven ids andediting.jsontwelve.portable.jsonnames none, which is why it is the onlyone that means anything against an anchor.
So this issue's framing was right and my earlier note above was wrong: the two
"unblocked" measurements are not a matter of pointing the existing harness
at the anchor. Each needs a request set written against the anchor's own rows —
which is the work, and it is not large, but it is work rather than a run.
What is genuinely in hand: the machinery (
WRITE_WORKLOADSalready gives awrite scenario a fresh fixture copy per repetition and merges the passes), the
gate that refuses to time a disagreement, and a published native upstream.
One more difference for the list, from the two dropped above: we serve an
image where upstream answers
404, for two rows of the anchor.#94 is closed, so the transcoding number is not blocked on it any more — and
now that it can be attempted, something else blocks it. Correcting my earlier
note, which took this issue's text on trust instead of checking.
Two things had to be fixed before it could even be attempted:
transcoding.jsonnamed six ids, all absent from the library. Everyrequest 404'd on both servers and agreed about it, so the scenario could not
fail — the same shape as #148 in another corner. Re-pointed in
55c938e.otherwise, and the scratch tmpfs is not it.
With those fixed the workload is live, both servers really transcode, and the
gate refuses to time it — five of the six disagree:
stream.mp4h264, maxWidth=320stream.mp4hevcstream.tsstream.mp4videoCodec=copy, audio→mp3/Audio/{flac}/universal?container=mp3200 audio/mpeg500 text/plainSo the answer to "how many concurrent sessions does this server sustain" is not
a number yet, and the reason is no longer hardware: we and upstream produce
different output for the same request, and timing that would compare two
different pieces of work. The gate is right to drop them.
The
500is the sharp end. A flac asked for as mp3 through/Audio/{id}/universalfails outright where upstream answers with audio, andthe same item asked for as aac does not fail the same way.
That, and the 20–53% size gap on three of four video shapes, want their own
issues; a review is diagnosing the
500now. Neither is a benchmark problem —they are correctness, and they are what this issue was going to run into.
Correcting the note above, and the numbers with it. A review found the six
ids were not absent — they were the
enrichedsnapshot's, whichdocs/benchmarks.mdsays is this workload's fixture. Running it againstdefaultis what made them look gone. The scenario was not dead; I had thewrong fixture.
The re-pointed ids resolve in both snapshots, so that is kept and the
document now says so. Three of the six were also measuring something other than
their name, fixed in
2a34f89:.tscase was the one number that read like a pass — 188 bytes apart— and it was not one. Night Signal is ninety seconds, so the rate cap binds
and both servers converge on it whatever their arguments say. Night Signal
belongs on
video-to-another-codec(it is the fixture's one hevc source);a five-second h264 clip takes the container change, which also restores the
remux coverage the tree had lost.
audio-from-flac-to-aacnamed notranscodingContainer, so upstreammuxed AAC with the MP3 muxer and died — and ours refuses the route — so the
two agreed about failing. With the container named it measures.
Corrected, on
default, every one of the six disagrees:stream.mp4h264, maxWidth=320stream.mp4hevc (Night Signal, 90 s)stream.ts(Red Harbour, 5 s)stream.mp4copy, audio→mp3/Audio/{flac}/universal→ mp3200 audio/mpeg500/Audio/{flac}/universal→ aac200500The
500is not a bug, it is a hole.crates/ephyra-http/src/routes/streaming/audio.rs:92-98refuses anything the negotiation does not settle as a direct play, with
"transcoding is not implemented". The encoder path it wants already exists —
videos.rs:503-560builds the request, starts the job and serves the growingfile — it is simply not wired to this route.
The size gap is real and the corpus cannot see it. It is not the bench
truncating: the byte counts come from the gate, which drains each body once per
server before any timing, compares
@lengthexactly, and has no entry hidingit. Both servers run one ffmpeg.
crates/ephyra-encoding/corpus/progressive-video.jsonholds 3,380 cases and none of them is this regime — every source is ≥720×576,
≥5 Mbps, stereo or better; the swept
-maxratebottoms out at 800,000 wherethese ask 200,000,
-abat 128,000 where they ask 64,000,maxWidthat 640where one asks 320, and
videoCodec=copywithaudioCodec=mp3has no case atall. A coverage gap, not leniency.
So the transcoding number this issue wants is still not obtainable, and the
reason is now specific: the two servers do different work for all six
requests. Timing that would compare two different things, which is what the
gate is for.
The artwork number, measured. One of the four this issue asks for.
The request set had to be written against the anchor's own rows, which is what
this issue says and what I confirmed the hard way —
artwork.jsonnames fiveenrichedids and answers nothing on an anchor. Ten requests of the sameshapes over a film and a person the anchor holds; both servers native and
pinned; five repetitions; the gate agreed on 8 of the 10 before anything was
timed, so this is a measurement rather than 404 routing.
Ours is flat at ~76 rps from c=8 to c=128 with latency growing linearly — a
queue in front of a CPU-bound step — while upstream is flat at ~18,500 rps with
a sub-millisecond median, which is a file being sent. The responses are the
same picture to within a percent.
The cause is the one this issue already suspected in a smaller form: "every
image request re-encodes, and the report notes our responses run 5% larger per
request than upstream's, unexplained." The 5% is not the story. Two hundred
and forty times is. Filed as #185.
The two requests that dropped are a
Personimage where upstream answers404and we answer with a picture — the same difference the drop set turned up.
So of the five things this issue wants: the drop set is re-derived, artwork is
measured, transcoding is blocked on all six of its requests disagreeing (#184
and the size gap), the write path needs the same treatment artwork just got —
a request set over anchor rows, and
WRITE_WORKLOADSalready gives it a freshfixture per repetition — and scanning beside upstream is not an HTTP request
set at all.
The scenario used here is not committed: it names one household's rows, and
reference/snapshots/shows the shape that problem already has. Where ananchor-specific request set should live is worth deciding before the write-path
one is written.
The write path, measured — and this issue's last piece. Closing it, split into
what is left.
The write path
A request set written against the anchor's own rows, which is what this issue
says and what the artwork run confirmed the hard way: twenty requests, a
playback session reported from start to stop, played/favourite/rating writes and
a posted
UserData, with a resume listing and two listings read while the writesgo on. Three repetitions, each from a fresh copy of the fixture, both servers
pinned to their own cores, the generator to its own.
19 of 20 agreed before anything was timed. The one that dropped is
/UserItems/Resume, onParentBackdropImageTags— the widest of the drop-setdifferences already recorded here.
Upstream tops out at ~300 writes a second and falls back to 269 at c=128; ours
climbs to ~630 and stays there. At a fixed rate (25/50/100 a second) both serve
every request and ours answers in a third to a half the time.
What the run also recorded, over 134,400 responses ours and 63,849 upstream:
So the WAL contention case this issue was written for does not bite: the
pragmas hold under a concurrent write load on a 56k-item library, and the
one thing to watch is peak RSS rather than throughput.
The request set is not committed — it names one household's rows, and a set
that answers nothing on any other fixture is worse in the repository than
beside the library it was written for.
b55a583teachescargo xtask benchtotake a request set by path (
--workload <file.json>,--writes <name>), whichis where that question this issue kept running into gets its answer.
Where the five things stand
recorded here rather than filed.
after
365cd2c, and faster than upstream at the tail.hardware: all six requests disagree because the two servers write different
files for the same request. Filed as #190 (the divergence) and #191 (the
measurement waiting behind it).
can be asked for it: the anchor has no media, the reference library is
nineteen five-second clips, and no upstream stands on the real one. Filed as
#192.
Three of the four numbers exist or are answered; the two that do not have issues
of their own now, each with its own blocker. Nothing is left under this heading.