Documentation
¶
Overview ¶
Command worstshape finds the document shape a handler set is slowest on, by running the same handlers over documents of the same size in different shapes and ranking them.
The point is that document size is the wrong axis. Held at 200 KB, the shapes below span a factor of about 1900 in time per byte, and what they span is handler calls per byte - not nesting depth, not markup validity, not how much of the document matches. Measured on an M3 Pro, fastest of seven passes, with a handler set of two element selectors and a document-level text and comment handler:
shape ns/byte alloc B/KB calls implied end tags (li) 103.565 19,667.8 120,000 malformed: unclosed tags 50.974 6,561.0 40,000 attribute-heavy anchors 42.233 3,284.0 17,646 many text nodes 41.267 7,290.0 44,444 tables 35.122 5,180.9 31,578 many siblings 29.497 2,986.3 18,181 deep nesting 25.376 2,986.9 18,183 many comments 16.926 2,055.7 25,000 bogus comments 13.478 1,646.1 20,000 malformed: stray end tags 3.144 8.5 0 one element, many attributes 1.601 8.6 1 one long comment 0.481 7.7 1 one long text node 0.076 8.8 2 raw text (script) 0.067 8.8 2 entities 0.065 8.0 2 one long attribute value 0.055 8.6 1
Three things worth taking from that.
The worst shape is a list of <li> with no closing tags, and it is worst because it produces three handler calls per element: the element itself, its text, and the empty final chunk that ends the text node. Forty thousand list items in 200 KB is 120,000 calls. It is not a pathological document - it is what a navigation menu looks like.
A document of stray end tags is nearly free, and that is B194 showing up as a cost: no handler ever sees a stray end tag, so 200 KB of them produce zero calls and allocate 8.5 bytes per KB, which is the floor. The same floor holds for one long text node, one long comment, a script, and one element with a 200 KB attribute value - all of them one or two calls.
So a rewrite's cost is set by how finely the document is divided, and a handler set that looks cheap on prose can be twenty times more expensive on markup of the same size. The allocation column is the same story and does not depend on the machine: 19.7 KB allocated per KB of input at the top, about 8.5 bytes per KB at the bottom.
The ranking is a property of the handler set, not of the library, which is why this is a harness rather than a table: point it at your own handlers.
One caveat about the ns/byte column. The cheapest shapes here take a few microseconds, and a clock that ticks every 340µs - the Windows runner's - reports them as exactly 0. That is the clock's answer rather than an error, so nothing here asserts a duration is above zero, and the gates are the allocation and call columns, which are the same number on every machine. The tool says so when it happens.