mikser-io 10.7.0 → 10.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/logger.js +63 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "10.7.0",
3
+ "version": "10.9.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/logger.js CHANGED
@@ -498,10 +498,21 @@ onLoad(() => {
498
498
  //
499
499
  // A minimum TOTAL would fix the count and get the other half wrong: four PDFs
500
500
  // through Chrome is a small phase and a slow one, and it is exactly the phase
501
- // worth narrating. Elapsed time is what "long" means, it needs no per-phase
502
- // tuning, and it applies to the drawn path too — `finished: 5 0s` is the same
503
- // non-information in a terminal, and its own `0s` says so.
504
- export const PROGRESS_MIN_MS = 1000
501
+ // worth narrating. Elapsed time is what "long" means and it needs no
502
+ // per-phase tuning.
503
+ //
504
+ // It is an INTERVAL rather than a threshold, and quartiles are gone with it.
505
+ // A quartile is a fraction of the WORK, so it says nothing about how often a
506
+ // line appears: four records land in three seconds on a fast phase and four
507
+ // records cover an hour on a slow one. Time is the axis a reader cares about,
508
+ // so one line per interval, however much work passed in between.
509
+ //
510
+ // This gates the RUNNING commentary only. `progress-finished` is not behind
511
+ // it: every phase says that it ran and what it cost, whatever the duration.
512
+ // Gated, a short phase produced no record at all and a build could not say
513
+ // what it had done — and off a TTY that line is the only place phase timings
514
+ // come from.
515
+ export const PROGRESS_INTERVAL_MS = 30_000
505
516
 
506
517
  // Which item, not just how far. A phase name alone says a build is doing
507
518
  // something; the thing a stuck build needs to say is WHAT it is stuck on.
@@ -519,6 +530,23 @@ function progressDetail(detail) {
519
530
  return relative.startsWith('..') ? text : relative
520
531
  }
521
532
 
533
+ // A duration rounded away to nothing is not a measurement.
534
+ //
535
+ // `finished: 5 0s` was the first version of this and `0s` said nothing;
536
+ // millisecond resolution moved the same defect one order of magnitude down,
537
+ // where nine of a plain build's thirteen phases still printed `0ms`. The
538
+ // clock is the cause rather than the format — Date.now() cannot resolve
539
+ // below a millisecond — so phases are timed with performance.now(), and each
540
+ // band is printed at the resolution it actually has. The floor exists
541
+ // because even a monotonic clock can report two identical readings, and
542
+ // `0.00ms` would be the same lie a third time.
543
+ export function formatDuration(ms) {
544
+ if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`
545
+ if (ms >= 1) return `${Math.round(ms)}ms`
546
+ if (ms >= 0.01) return `${ms.toFixed(2)}ms`
547
+ return '<0.01ms'
548
+ }
549
+
522
550
  function emitProgress({ name, total, value, detail }) {
523
551
  const at = progressDetail(detail)
524
552
  const fields = { code: 'progress', phase: name, total, value }
@@ -551,7 +579,8 @@ export function trackProgress(name, total) {
551
579
  // --json would have extended that to every machine reading the output.
552
580
  // Losing the graphics is the point; losing the information is not.
553
581
  const drawn = !carriesDocument && Boolean(process.stdout.isTTY) && Boolean(runtime.options.info)
554
- currentBar = { name, total, value: 0, started: Date.now(), drawn, milestone: 0, detail: null }
582
+ const now = performance.now()
583
+ currentBar = { name, total, value: 0, started: now, drawn, lastReport: now, detail: null }
555
584
  if (drawn) ensureGauge().show({ section: name, subsection: `0/${total}` }, 0)
556
585
  }
557
586
 
@@ -567,17 +596,15 @@ export function updateProgress(detail) {
567
596
  if (drawn) {
568
597
  gauge?.show({ section: name, subsection: `${value}/${total}` }, value / total)
569
598
  } else {
570
- // Quartiles, not every item: a bar redraws in place and costs one
571
- // line, a log record does not. 800 documents is 800 lines of noise if
572
- // this counts the way the bar does.
573
- //
574
- // The milestone advances whether or not the record is emitted. Held
575
- // back, every quartile the phase passed under the threshold would fire
576
- // in a burst the moment it crossed.
577
- const reached = Math.floor((value / total) * 4)
578
- if (reached > currentBar.milestone && value < total) {
579
- currentBar.milestone = reached
580
- if (Date.now() - currentBar.started >= PROGRESS_MIN_MS) emitProgress(currentBar)
599
+ // One line per interval, not per item and not per quartile: a bar
600
+ // redraws in place and costs one line, a log record does not, so 800
601
+ // documents is 800 lines of noise if this counts the way the bar does.
602
+ // A phase that finishes inside the interval says nothing at all while
603
+ // it runs its finished line covers it.
604
+ const now = performance.now()
605
+ if (now - currentBar.lastReport >= PROGRESS_INTERVAL_MS && value < total) {
606
+ currentBar.lastReport = now
607
+ emitProgress(currentBar)
581
608
  }
582
609
  }
583
610
  if (value >= total) stopProgress()
@@ -587,19 +614,33 @@ export function stopProgress() {
587
614
  if (!currentBar) return
588
615
  const logger = useLogger()
589
616
  const { name, total, value, started } = currentBar
590
- const ms = Date.now() - started
617
+ // Rounded so the field carries real sub-millisecond precision without
618
+ // float noise; the message formats it separately.
619
+ const ms = Math.round((performance.now() - started) * 1000) / 1000
591
620
  gauge?.hide()
592
621
  // Structured either way, so a machine reading --json's stderr gets the
593
622
  // same facts a person reads off the bar.
594
623
  //
595
- // A phase that did not finish is worth a line at any duration — it is a
596
- // warning, not progress, and the whole point is that it is unexpected.
624
+ // This line has to stand alone. With the running commentary on an
625
+ // interval it is the ONLY record most phases produce, and `Documents
626
+ // import finished: 5 0s` said neither what the five were nor how long it
627
+ // took — a count with no subject and a duration rounded away to nothing.
628
+ //
629
+ // The subject is the PHASE, not an item. The last entity a phase happened
630
+ // to walk is not what the phase was about: seven journal phases in a row
631
+ // reported the same `/layouts/page.hbs` because that is where the walk
632
+ // ended, and `Files import finished: 3, last .../social-fb.svg` put a
633
+ // filename into the build log that nothing had anything to say about — it
634
+ // broke a test asserting that file is never mentioned, which is exactly
635
+ // the misreading it invites. `Files import finished: 3 in 3ms` already
636
+ // says what finished, how much of it, and what it cost. The running
637
+ // records keep the item, because there it shows MOVEMENT.
597
638
  if (value < total) {
598
639
  logger.warn({ code: 'progress-unfinished', phase: name, total, value, missing: total - value },
599
- '%s unfinished: %d', name, total - value)
600
- } else if (ms >= PROGRESS_MIN_MS) {
640
+ '%s unfinished: %d of %d after %s', name, total - value, total, formatDuration(ms))
641
+ } else {
601
642
  logger.info({ code: 'progress-finished', phase: name, total, ms },
602
- '%s finished: %d %ds', name, total, Math.round(ms / 1000))
643
+ '%s finished: %d in %s', name, total, formatDuration(ms))
603
644
  }
604
645
  currentBar = null
605
646
  }