@@ -306,6 +306,61 @@ const SEPARATOR = " ";
306306 */
307307const WIDTHS_PER_RENDER = 1024 ;
308308
309+ /**
310+ * How many space-separated parts one measuring render may carry.
311+ *
312+ * The element budget is not the engine's only one. Every part of every text a
313+ * render measures is kept in a cache the engine refuses from, and that cache
314+ * holds the same number of them however few elements carry them: measured on a
315+ * freshly built engine, one text of 16,384 parts and 1,366 texts of twelve each
316+ * refuse, while 16,383 parts go through either way. A tab and a newline are not
317+ * separators there, a run of spaces produces an empty part that costs what a
318+ * word costs, and two texts spelled the same share one entry.
319+ *
320+ * This is the budget whose refusal the renderer's recovery cannot absorb: a
321+ * replacement instance is given the same cache, so a render that overran it
322+ * overruns the replacement too and the refusal reaches the screen as the error
323+ * that ends it. Half the engine's own figure, which leaves the parts an
324+ * instance is still holding from the render before this one somewhere to sit.
325+ */
326+ const WORDS_PER_RENDER = 8192 ;
327+
328+ /** What one text costs that budget, counted the way the engine counts it. */
329+ function wordsIn ( text : string ) : number {
330+ let parts = 1 ;
331+ for ( let at = 0 ; at < text . length ; at += 1 ) {
332+ if ( text [ at ] === " " ) {
333+ parts += 1 ;
334+ }
335+ }
336+ return parts ;
337+ }
338+
339+ /**
340+ * How much of one line could still fit a region, as a length in code units.
341+ *
342+ * A single text is bounded by no batch size, and one line of somebody's output
343+ * can hold more parts than a render measures — a journal of this product holds
344+ * lines of 19,999 — so a line is measured as far as it could still fit and
345+ * never further. That prefix settles the question it is asked: every part after
346+ * the first is preceded by a space, so `room + 2` of them are already wider
347+ * than `room` columns, and text does not narrow when more of it is appended.
348+ * The prefix ends at a space, which is a grapheme boundary of its own.
349+ */
350+ function windowOf ( text : string , room : number ) : number {
351+ const parts = Math . min ( room + 2 , WORDS_PER_RENDER ) ;
352+ let seen = 0 ;
353+ for ( let at = 0 ; at < text . length ; at += 1 ) {
354+ if ( text [ at ] === " " ) {
355+ seen += 1 ;
356+ if ( seen === parts ) {
357+ return at ;
358+ }
359+ }
360+ }
361+ return text . length ;
362+ }
363+
309364/**
310365 * Ask the engine how wide each of these texts is.
311366 *
@@ -324,12 +379,26 @@ function* widths(
324379 return Ok ( [ ] ) ;
325380 }
326381 const answers : number [ ] = [ ] ;
327- for ( let from = 0 ; from < texts . length ; from += WIDTHS_PER_RENDER ) {
328- const batch = yield * batchWidths ( renderer , size , texts . slice ( from , from + WIDTHS_PER_RENDER ) ) ;
382+ let from = 0 ;
383+ while ( from < texts . length ) {
384+ let to = from ;
385+ let words = 0 ;
386+ while ( to < texts . length && to - from < WIDTHS_PER_RENDER ) {
387+ const cost = wordsIn ( texts [ to ] ) ;
388+ // A text that outgrows the budget on its own still goes, alone, so the
389+ // engine's refusal names that text rather than this loop never advancing.
390+ if ( to > from && words + cost > WORDS_PER_RENDER ) {
391+ break ;
392+ }
393+ words += cost ;
394+ to += 1 ;
395+ }
396+ const batch = yield * batchWidths ( renderer , size , texts . slice ( from , to ) ) ;
329397 if ( ! batch . ok ) {
330398 return batch ;
331399 }
332400 answers . push ( ...batch . value ) ;
401+ from = to ;
333402 }
334403 return Ok ( answers ) ;
335404}
@@ -679,11 +748,15 @@ function* fitLine(
679748 if ( line . text . length === 0 ) {
680749 return Ok ( [ piece ( 0 , 0 , false , 0 ) ] ) ;
681750 }
682- const whole = yield * widths ( renderer , size , [ line . text ] ) ;
683- if ( ! whole . ok ) {
684- return whole ;
751+ // Asked of as much of the line as the region could still hold. One row is a
752+ // line the engine measured as fitting, and a line held back from the question
753+ // is longer than the answer it would have given, so it is cut.
754+ const reach = windowOf ( line . text , room ) ;
755+ const reaching = yield * widths ( renderer , size , [ line . text . slice ( 0 , reach ) ] ) ;
756+ if ( ! reaching . ok ) {
757+ return reaching ;
685758 }
686- if ( whole . value [ 0 ] <= room ) {
759+ if ( reach === line . text . length && reaching . value [ 0 ] <= room ) {
687760 return Ok ( [ piece ( 0 , line . text . length , false , 0 ) ] ) ;
688761 }
689762 // Where this line begins, so its continuations begin there too. Bounded well
@@ -728,7 +801,10 @@ function* cutAt(
728801 rest : string ,
729802 room : number ,
730803) : Operation < Result < number > > {
731- const stops = graphemeStops ( rest ) ;
804+ // Over the boundaries of the prefix that could still fit, which is what keeps
805+ // every candidate inside one render's budget and keeps this from segmenting
806+ // the whole remainder once per row of a very long line.
807+ const stops = graphemeStops ( rest . slice ( 0 , windowOf ( rest , room ) ) ) ;
732808 // Narrow by halves over grapheme boundaries, asking the engine for one batch
733809 // of candidates per round rather than one render per candidate.
734810 let low = 0 ;
0 commit comments