@trendr/core 0.4.4 → 0.4.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendr/core",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Pyers",
package/src/ansi.js CHANGED
@@ -97,8 +97,21 @@ function rgbToHex(r, g, b) {
97
97
  return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
98
98
  }
99
99
 
100
- export function parseSgr(params, state) {
101
- if (!state) state = { fg: null, bg: null, attrs: 0 }
100
+ // a params string reduces to a state-independent delta: fg/bg values when
101
+ // touched (undefined = untouched), plus and/or masks over attrs. styled text
102
+ // repeats the same handful of sequences thousands of times per frame, so
103
+ // each is parsed once and its delta replayed from cache
104
+ const sgrDeltaCache = new Map()
105
+ const SGR_CACHE_MAX = 4000
106
+
107
+ const setBit = (delta, bit) => { delta.or |= bit }
108
+ const clearBits = (delta, bits) => { delta.and &= ~bits; delta.or &= ~bits }
109
+
110
+ function sgrDelta(params) {
111
+ const cached = sgrDeltaCache.get(params)
112
+ if (cached) return cached
113
+
114
+ const delta = { fg: undefined, bg: undefined, and: ~0, or: 0 }
102
115
  const codes = params.split(';').map(Number)
103
116
  let i = 0
104
117
 
@@ -106,46 +119,58 @@ export function parseSgr(params, state) {
106
119
  const c = codes[i]
107
120
 
108
121
  if (c === 0) {
109
- state.fg = null
110
- state.bg = null
111
- state.attrs = 0
112
- } else if (c === 1) state.attrs |= BOLD
113
- else if (c === 2) state.attrs |= DIM
114
- else if (c === 3) state.attrs |= ITALIC
115
- else if (c === 4) state.attrs |= UNDERLINE
116
- else if (c === 7) state.attrs |= INVERSE
117
- else if (c === 9) state.attrs |= STRIKETHROUGH
118
- else if (c === 22) state.attrs &= ~(BOLD | DIM)
119
- else if (c === 23) state.attrs &= ~ITALIC
120
- else if (c === 24) state.attrs &= ~UNDERLINE
121
- else if (c === 27) state.attrs &= ~INVERSE
122
- else if (c === 29) state.attrs &= ~STRIKETHROUGH
123
- else if (c === 39) state.fg = null
124
- else if (c === 49) state.bg = null
122
+ delta.fg = null
123
+ delta.bg = null
124
+ delta.and = 0
125
+ delta.or = 0
126
+ } else if (c === 1) setBit(delta, BOLD)
127
+ else if (c === 2) setBit(delta, DIM)
128
+ else if (c === 3) setBit(delta, ITALIC)
129
+ else if (c === 4) setBit(delta, UNDERLINE)
130
+ else if (c === 7) setBit(delta, INVERSE)
131
+ else if (c === 9) setBit(delta, STRIKETHROUGH)
132
+ else if (c === 22) clearBits(delta, BOLD | DIM)
133
+ else if (c === 23) clearBits(delta, ITALIC)
134
+ else if (c === 24) clearBits(delta, UNDERLINE)
135
+ else if (c === 27) clearBits(delta, INVERSE)
136
+ else if (c === 29) clearBits(delta, STRIKETHROUGH)
137
+ else if (c === 39) delta.fg = null
138
+ else if (c === 49) delta.bg = null
125
139
  else if (c === 38 && codes[i + 1] === 5) {
126
- state.fg = indexToColor(codes[i + 2])
140
+ delta.fg = indexToColor(codes[i + 2])
127
141
  i += 2
128
142
  } else if (c === 48 && codes[i + 1] === 5) {
129
- state.bg = indexToColor(codes[i + 2])
143
+ delta.bg = indexToColor(codes[i + 2])
130
144
  i += 2
131
145
  } else if (c === 38 && codes[i + 1] === 2) {
132
- state.fg = rgbToHex(codes[i + 2], codes[i + 3], codes[i + 4])
146
+ delta.fg = rgbToHex(codes[i + 2], codes[i + 3], codes[i + 4])
133
147
  i += 4
134
148
  } else if (c === 48 && codes[i + 1] === 2) {
135
- state.bg = rgbToHex(codes[i + 2], codes[i + 3], codes[i + 4])
149
+ delta.bg = rgbToHex(codes[i + 2], codes[i + 3], codes[i + 4])
136
150
  i += 4
137
151
  } else {
138
152
  const fgIdx = basicFgToIndex(c)
139
- if (fgIdx != null) state.fg = indexToColor(fgIdx)
153
+ if (fgIdx != null) delta.fg = indexToColor(fgIdx)
140
154
  else {
141
155
  const bgIdx = basicBgToIndex(c)
142
- if (bgIdx != null) state.bg = indexToColor(bgIdx)
156
+ if (bgIdx != null) delta.bg = indexToColor(bgIdx)
143
157
  }
144
158
  }
145
159
 
146
160
  i++
147
161
  }
148
162
 
163
+ if (sgrDeltaCache.size >= SGR_CACHE_MAX) sgrDeltaCache.clear()
164
+ sgrDeltaCache.set(params, delta)
165
+ return delta
166
+ }
167
+
168
+ export function parseSgr(params, state) {
169
+ if (!state) state = { fg: null, bg: null, attrs: 0 }
170
+ const delta = sgrDelta(params)
171
+ if (delta.fg !== undefined) state.fg = delta.fg
172
+ if (delta.bg !== undefined) state.bg = delta.bg
173
+ state.attrs = (state.attrs & delta.and) | delta.or
149
174
  return state
150
175
  }
151
176
 
package/src/diff-view.js CHANGED
@@ -38,6 +38,32 @@ function intraSegments(ansiLine, plainLen, ranges) {
38
38
  return segs
39
39
  }
40
40
 
41
+ // components re-execute every frame but diff inputs are immutable strings;
42
+ // recomputing a word diff per card per frame dominates long transcripts.
43
+ // whole mode (before/after strings) is memoized; patch/hunks callers pass
44
+ // fresh objects per render, so they fall through to a direct compute
45
+ const diffMemo = new Map()
46
+ const DIFF_MEMO_MAX = 300
47
+ const hiLineCache = new Map()
48
+ const HI_CACHE_MAX = 2000
49
+
50
+ function memoComputeDiff({ before, after, patch, hunks, wordDiff, context }) {
51
+ if (patch != null || hunks != null || before == null || after == null) {
52
+ return computeDiff({ before, after, patch, hunks, wordDiff, context })
53
+ }
54
+ let byAfter = diffMemo.get(before)
55
+ if (!byAfter) {
56
+ if (diffMemo.size >= DIFF_MEMO_MAX) diffMemo.clear()
57
+ diffMemo.set(before, (byAfter = new Map()))
58
+ }
59
+ const key = `${wordDiff}|${context}|${after.length}\x00${after.slice(0, 64)}`
60
+ const inner = byAfter.get(key)
61
+ if (inner && inner.after === after) return inner.result
62
+ const result = computeDiff({ before, after, wordDiff, context })
63
+ byAfter.set(key, { after, result })
64
+ return result
65
+ }
66
+
41
67
  export function Diff({
42
68
  before,
43
69
  after,
@@ -62,14 +88,16 @@ export function Diff({
62
88
  const offset = offsetProp ?? offsetInternal()
63
89
  const setOffset = onScroll ?? setOffsetInternal
64
90
 
65
- const { rows, stats } = computeDiff({ before, after, patch, hunks, wordDiff, context })
91
+ const { rows, stats } = memoComputeDiff({ before, after, patch, hunks, wordDiff, context })
66
92
 
67
93
  const wholeMode = hunks == null && patch == null
68
- const hiCache = new Map()
69
94
  const hiLines = (text) => {
70
- if (hiCache.has(text)) return hiCache.get(text)
95
+ const key = `${language}\x00${text}`
96
+ const cached = hiLineCache.get(key)
97
+ if (cached) return cached
71
98
  const lines = (highlight ? highlight(text, language) : text).split('\n')
72
- hiCache.set(text, lines)
99
+ if (hiLineCache.size >= HI_CACHE_MAX) hiLineCache.clear()
100
+ hiLineCache.set(key, lines)
73
101
  return lines
74
102
  }
75
103
  const beforeHi = wholeMode && highlight ? hiLines(before ?? '') : null
package/src/renderer.js CHANGED
@@ -948,6 +948,7 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
948
948
  let lastInlineBuf = null
949
949
 
950
950
  function frame() {
951
+ const frameStart = performance.now()
951
952
  const prevCtx = activeContext
952
953
  activeContext = ctx
953
954
  overlays = []
@@ -1076,7 +1077,7 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
1076
1077
  }
1077
1078
  lastFrameTimestamp = now
1078
1079
  const avgMs = frameTimeWindow.length > 0 ? frameTimeWindow.reduce((a, b) => a + b, 0) / frameTimeWindow.length : 16.67
1079
- lastFrameStats = { changed, total: width * height, bytes: output ? Buffer.byteLength(output) : 0, fps: Math.round(1000 / avgMs) }
1080
+ lastFrameStats = { changed, total: width * height, bytes: output ? Buffer.byteLength(output) : 0, fps: Math.round(1000 / avgMs), renderMs: performance.now() - frameStart }
1080
1081
 
1081
1082
  const tmp = prev
1082
1083
  prev = curr
package/src/wrap.js CHANGED
@@ -34,7 +34,14 @@ const TAB_STOP = 8
34
34
 
35
35
  const tabWidth = (col) => TAB_STOP - (col % TAB_STOP)
36
36
 
37
+ // text nodes are measured every layout pass but their strings are immutable;
38
+ // transcripts re-measure thousands of identical strings per frame
39
+ const measureCache = new Map()
40
+ const MEASURE_CACHE_MAX = 20000
41
+
37
42
  export function measureText(text) {
43
+ const hit = measureCache.get(text)
44
+ if (hit !== undefined) return hit
38
45
  const clean = stripAnsi(text)
39
46
  let width = 0
40
47
  for (let i = 0; i < clean.length; i++) {
@@ -42,6 +49,8 @@ export function measureText(text) {
42
49
  if (code > 0xffff) i++
43
50
  width += code === 9 ? tabWidth(width) : charWidth(code)
44
51
  }
52
+ if (measureCache.size >= MEASURE_CACHE_MAX) measureCache.clear()
53
+ measureCache.set(text, width)
45
54
  return width
46
55
  }
47
56