fix(js): read printd literals in linear time

util.printd measured the run of identical characters at every position of
the format, literals included, but copied a literal one character at a time.
A format with a long stretch of one literal character rescanned the rest of
that stretch at every step: quadratic work from one script-built string.
Only the date letters are read as runs now.
This commit is contained in:
aiongg
2026-10-22 17:22:50 +00:00
parent 77480498b2
commit 2e9b247f3f
2 changed files with 20 additions and 1 deletions
+9 -1
View File
@@ -752,7 +752,11 @@ fn printd(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue>
let mut i = 0;
while i < b.len() {
let c = b[i];
let run = b[i..].iter().take_while(|x| **x == c).count();
// Only date letters read as runs; a literal is copied one character at a time, so
// measuring its run would rescan a long literal stretch at every position.
let run = if b"ymdHhMst".contains(&c) { b[i..].iter().take_while(|x| **x == c).count() } else { 1 };
#[cfg(test)]
PRINTD_SCANNED.fetch_add(run as u64, std::sync::atomic::Ordering::Relaxed);
let push_num = |out: &mut String, v: i64, n: usize| {
if n >= 2 {
out.push_str(&format!("{v:02}"));
@@ -1011,6 +1015,10 @@ fn event_object(ctx: &mut Context, e: &Event) -> JsObject {
// stack a script can use: brackets and prefix operators by the limits below, and every other
// construct (a few KiB per level, several bytes of source each) by the length limit.
/// Bytes `util.printd` has scanned for runs of a format character, for tests that bound its work.
#[cfg(test)]
pub(crate) static PRINTD_SCANNED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// Longest script, in bytes.
pub(crate) const MAX_SCRIPT_BYTES: usize = 256 * 1024;
/// Largest width or precision accepted by `util.printf`.
+11
View File
@@ -244,6 +244,17 @@ fn document_requests_and_console() {
);
}
#[test]
fn printd_reads_a_long_literal_run_in_linear_work() {
crate::PRINTD_SCANNED.store(0, std::sync::atomic::Ordering::Relaxed);
let o = go("event.value = util.printd(new Array(20001).join('x'), new Date(2024, 0, 5)).length;", &Event::field("Calculate", "total", ""));
assert_eq!(o.error, None);
assert_eq!(o.value, "20000");
// Other tests format short dates at the same time; they add a few dozen bytes at most.
let scanned = crate::PRINTD_SCANNED.load(std::sync::atomic::Ordering::Relaxed);
assert!(scanned < 40_000, "printd scanned {scanned} bytes for a 20000-byte format");
}
#[test]
fn document_level_functions_are_available() {
let o = run(