Skip to content

Commit 7edf2f1

Browse files
committed
Try to reduce allocations during render a little
1 parent be6d84d commit 7edf2f1

2 files changed

Lines changed: 149 additions & 66 deletions

File tree

src/draw.ts

Lines changed: 118 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { combinePatterns, getBoundariesFromPattern, getScreenSize } from './functions'
1+
import {
2+
combinePatterns,
3+
getBoundariesFromPattern,
4+
getScreenSize,
5+
mapPatternToViewportSpace,
6+
mutateBoundariesFromPattern,
7+
} from './functions'
28
import { Queue } from './queue'
3-
import { AbsoluteNumber, AbsolutePattern, Boundaries, State } from './types'
9+
import { Boundaries, Size, State, ViewportNumber, ViewportPattern } from './types'
410

511
// -- constants
612

@@ -12,34 +18,38 @@ const COLORS = '0123456789abcdef'
1218

1319
const MIN_DEPTH = 3
1420
const MAX_DEPTH = Infinity
15-
const MAX_PREVIEW_DRAW_CALLS = 1e4 // number of shapes to draw per preview frame
21+
const MAX_PREVIEW_DRAW_CALLS = 5e3 // number of shapes to draw per preview frame
1622
const MAX_DRAW_TIME_MS = 15 // how long to draw a frame in ms
1723
const MAX_QUEUE_SIZE = 1e6
18-
const MIN_PATTERN_SIZE = 0.0005 // ignore patterns where either side is smaller than this
24+
// const MIN_PATTERN_SIZE = 0.0005 // ignore patterns where either side is smaller than this
25+
const MIN_PATTERN_SIZE_PX = 1 // ignore patterns where either side is smaller than this
1926
const DEBUG = true as boolean
2027

2128
// We'll ignore everything that's a bit outside the viewport.
2229
// This is not really accurate, but should be OK for our purposes.
23-
const VALID_BOUNDARIES: Boundaries<AbsoluteNumber> = {
24-
xMin: -0.1 as AbsoluteNumber,
25-
xMax: 1.1 as AbsoluteNumber,
26-
yMin: -0.1 as AbsoluteNumber,
27-
yMax: 1.1 as AbsoluteNumber,
30+
function getViewportBoundaries(screenSize: Size): Boundaries<ViewportNumber> {
31+
return {
32+
xMin: (-0.1 * screenSize[0]) as ViewportNumber,
33+
xMax: (1.1 * screenSize[0]) as ViewportNumber,
34+
yMin: (-0.1 * screenSize[1]) as ViewportNumber,
35+
yMax: (1.1 * screenSize[1]) as ViewportNumber,
36+
}
2837
}
2938

30-
const isValidPattern = (pattern: AbsolutePattern): boolean => {
31-
const { xMin, xMax, yMin, yMax } = getBoundariesFromPattern(pattern)
32-
39+
const isValidPattern = (
40+
patternBoundaries: Boundaries<ViewportNumber>,
41+
viewportBoundaries: Boundaries<ViewportNumber>
42+
): boolean => {
3343
return (
3444
// Pattern fulfills minimum size.
35-
xMax - xMin >= MIN_PATTERN_SIZE &&
36-
yMax - yMin >= MIN_PATTERN_SIZE &&
45+
patternBoundaries.xMax - patternBoundaries.xMin >= MIN_PATTERN_SIZE_PX &&
46+
patternBoundaries.yMax - patternBoundaries.yMin >= MIN_PATTERN_SIZE_PX &&
3747
// X and Y ranges overlap with the valid boundaries.
3848
// We will only render patterns if at least one corner is inside the valid boundaries.
39-
xMin <= VALID_BOUNDARIES.xMax &&
40-
xMax >= VALID_BOUNDARIES.xMin &&
41-
yMin <= VALID_BOUNDARIES.yMax &&
42-
yMax >= VALID_BOUNDARIES.yMin
49+
patternBoundaries.xMin <= viewportBoundaries.xMax &&
50+
patternBoundaries.xMax >= viewportBoundaries.xMin &&
51+
patternBoundaries.yMin <= viewportBoundaries.yMax &&
52+
patternBoundaries.yMax >= viewportBoundaries.yMin
4353
)
4454
}
4555

@@ -52,7 +62,7 @@ const measure = (f: () => void): number => {
5262
}
5363

5464
type QueueEntry = {
55-
currentPattern: AbsolutePattern
65+
currentPattern: ViewportPattern
5666
depth: number
5767
}
5868

@@ -62,20 +72,64 @@ const patternQueue = new Queue<QueueEntry>({
6272
size: MAX_QUEUE_SIZE,
6373
})
6474

65-
function* generateDrawQueue(state: State): Generator<QueueEntry, void, void> {
75+
/**
76+
* Creates a generator that yields drawable patterns one by one.
77+
*
78+
* It starts with the initial screens and recursively applies all defined patterns
79+
* from the state. Each yielded pattern includes its generation depth.
80+
* The generation proceeds in a breadth-first manner.
81+
*
82+
* @param state The current application state, containing screens and patterns.
83+
* @param screenSize The current size of the screen/viewport.
84+
* @returns A generator yielding `QueueEntry` objects ({ currentPattern: ViewportPattern, depth: number }).
85+
*/
86+
function* streamDrawablePatterns({
87+
state,
88+
screenSize,
89+
}: {
90+
state: State
91+
screenSize: Size
92+
}): Generator<QueueEntry, void, void> {
6693
console.log(
6794
`generateDrawQueue start. Initial screens: ${state.screens.length}, Patterns: ${state.patterns.length}`
6895
)
6996

97+
const viewportBoundaries = getViewportBoundaries(screenSize)
98+
7099
patternQueue.clear()
71100

72101
for (const screen of state.screens) {
73102
patternQueue.push({
74-
currentPattern: screen,
103+
currentPattern: mapPatternToViewportSpace(screen, screenSize),
75104
depth: 0,
76105
})
77106
}
78107

108+
// Optimization: Only allocate boundaries object once and mutate it in place.
109+
// Make sure to update this object before using it!
110+
const patternBoundaries: Boundaries<ViewportNumber> = {
111+
xMin: 0 as ViewportNumber,
112+
xMax: 0 as ViewportNumber,
113+
yMin: 0 as ViewportNumber,
114+
yMax: 0 as ViewportNumber,
115+
}
116+
117+
function isValidPattern(pattern: ViewportPattern): boolean {
118+
mutateBoundariesFromPattern(pattern, patternBoundaries)
119+
120+
return (
121+
// Pattern fulfills minimum size.
122+
patternBoundaries.xMax - patternBoundaries.xMin >= MIN_PATTERN_SIZE_PX &&
123+
patternBoundaries.yMax - patternBoundaries.yMin >= MIN_PATTERN_SIZE_PX &&
124+
// X and Y ranges overlap with the valid boundaries.
125+
// We will only render patterns if at least one corner is inside the valid boundaries.
126+
patternBoundaries.xMin <= viewportBoundaries.xMax &&
127+
patternBoundaries.xMax >= viewportBoundaries.xMin &&
128+
patternBoundaries.yMin <= viewportBoundaries.yMax &&
129+
patternBoundaries.yMax >= viewportBoundaries.yMin
130+
)
131+
}
132+
79133
let iterations = 0
80134
while (patternQueue.size > 0) {
81135
iterations += 1
@@ -93,11 +147,11 @@ function* generateDrawQueue(state: State): Generator<QueueEntry, void, void> {
93147
}
94148

95149
for (const pattern of state.patterns) {
96-
const virtualScreen = combinePatterns(entry.currentPattern, pattern)
150+
const viewportPattern = combinePatterns(entry.currentPattern, pattern)
97151

98-
if (isValidPattern(virtualScreen)) {
152+
if (isValidPattern(viewportPattern)) {
99153
patternQueue.push({
100-
currentPattern: virtualScreen,
154+
currentPattern: viewportPattern,
101155
depth: entry.depth + 1,
102156
})
103157

@@ -119,11 +173,30 @@ function* generateDrawQueue(state: State): Generator<QueueEntry, void, void> {
119173

120174
type Chunk = {
121175
depth: number
122-
patterns: AbsolutePattern[]
176+
patterns: ViewportPattern[]
123177
}
124178

125-
function* generateChunkedDraws(state: State, chunkSize: number): Generator<Chunk, void, void> {
126-
const drawQueueIterator = generateDrawQueue(state)
179+
/**
180+
* Creates a generator that groups drawable patterns from `streamDrawablePatterns` into chunks.
181+
*
182+
* Patterns are batched together based on their generation depth or a maximum
183+
* chunk size. This is useful for processing or rendering patterns in groups.
184+
*
185+
* @param state The current application state.
186+
* @param chunkSize The maximum number of patterns to include in a single chunk.
187+
* @param screenSize The current size of the screen/viewport.
188+
* @returns A generator yielding `Chunk` objects ({ depth: number, patterns: ViewportPattern[] }).
189+
*/
190+
function* streamBatchedDrawablePatterns({
191+
state,
192+
chunkSize,
193+
screenSize,
194+
}: {
195+
state: State
196+
chunkSize: number
197+
screenSize: Size
198+
}): Generator<Chunk, void, void> {
199+
const drawQueueIterator = streamDrawablePatterns({ state, screenSize })
127200

128201
let chunk: Chunk = {
129202
depth: 0,
@@ -176,7 +249,7 @@ export function* drawFrameIncrementally(
176249

177250
const screenSize = getScreenSize(ctx)
178251

179-
const chunkedDrawsIterator = generateChunkedDraws(state, 1000)
252+
const batchedPatternsIterator = streamBatchedDrawablePatterns({ state, chunkSize: 1000, screenSize })
180253

181254
let iterations = 0
182255

@@ -189,7 +262,7 @@ export function* drawFrameIncrementally(
189262
const start = performance.now()
190263

191264
while (true) {
192-
const iteratorResult = chunkedDrawsIterator.next()
265+
const iteratorResult = batchedPatternsIterator.next()
193266

194267
if (iteratorResult.done) break
195268

@@ -201,32 +274,24 @@ export function* drawFrameIncrementally(
201274
ctx.strokeStyle = COLORS[Math.min(COLORS.length - 1, depth)]!
202275
ctx.beginPath()
203276

204-
// Optimization: Only allocate variables once.
205-
let xMin: number
206-
let yMin: number
207-
let xMax: number
208-
let yMax: number
209-
let x1: number
210-
let y1: number
211-
let x2: number
212-
let y2: number
213-
214-
for (const absolutePattern of patterns) {
215-
// Convert to viewport space.
216-
x1 = Math.round(absolutePattern.anchor[0] * screenSize[0])
217-
y1 = Math.round(absolutePattern.anchor[1] * screenSize[1])
218-
x2 = Math.round(absolutePattern.target[0] * screenSize[0])
219-
y2 = Math.round(absolutePattern.target[1] * screenSize[1])
220-
221-
// Get boundaries.
222-
// Optimization: Do not use getBoundariesFromPattern because it creates too many objects.
223-
xMin = Math.min(x1, x2)
224-
yMin = Math.min(y1, y2)
225-
xMax = Math.max(x1, x2)
226-
yMax = Math.max(y1, y2)
277+
// Optimization: Only allocate boundaries object once and mutate it in place.
278+
const boundaries: Boundaries<ViewportNumber> = {
279+
xMin: 0 as ViewportNumber,
280+
xMax: 0 as ViewportNumber,
281+
yMin: 0 as ViewportNumber,
282+
yMax: 0 as ViewportNumber,
283+
}
284+
285+
for (const viewportPattern of patterns) {
286+
mutateBoundariesFromPattern(viewportPattern, boundaries)
227287

228288
// Draw rectangle.
229-
ctx.rect(xMin, yMin, xMax - xMin, yMax - yMin)
289+
ctx.rect(
290+
boundaries.xMin,
291+
boundaries.yMin,
292+
boundaries.xMax - boundaries.xMin,
293+
boundaries.yMax - boundaries.yMin
294+
)
230295
}
231296

232297
ctx.fill()

src/functions.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import {
2-
AbsoluteNumber,
32
AbsolutePattern,
43
AbsolutePoint,
54
Boundaries,
65
NumberPair,
76
Pattern,
87
PatternNumber,
98
Point,
10-
RelativeNumber,
119
RelativePattern,
1210
RelativePoint,
1311
Size,
@@ -16,20 +14,20 @@ import {
1614
ViewportPoint
1715
} from './types'
1816

19-
function min<N extends PatternNumber>(a: N, b: N): N {
20-
return a < b ? a : b
17+
export function minPatternNumber<N extends PatternNumber>(a: N, b: N): N {
18+
return Math.min(a, b) as N
2119
}
2220

23-
function max<N extends PatternNumber>(a: N, b: N): N {
24-
return a > b ? a : b
21+
export function maxPatternNumber<N extends PatternNumber>(a: N, b: N): N {
22+
return Math.max(a, b) as N
2523
}
2624

2725
const getBoundariesFromTwoPoints = <N extends PatternNumber>([x1, y1]: Point<N>, [x2, y2]: Point<N>): Boundaries<N> => {
2826
return {
29-
xMin: min(x1, x2),
30-
xMax: max(x1, x2),
31-
yMin: min(y1, y2),
32-
yMax: max(y1, y2),
27+
xMin: minPatternNumber(x1, x2),
28+
xMax: maxPatternNumber(x1, x2),
29+
yMin: minPatternNumber(y1, y2),
30+
yMax: maxPatternNumber(y1, y2),
3331
}
3432
}
3533

@@ -42,10 +40,30 @@ export const normalizePattern = <N extends PatternNumber>(pattern: Pattern<N>):
4240
}
4341
}
4442

43+
/**
44+
* This function mutates the boundaries object in place.
45+
*
46+
* This is a performance optimization to avoid creating new objects, since this function is called very frequently
47+
* during rendering.
48+
*/
49+
export function mutateBoundariesFromPattern<N extends PatternNumber>(pattern: Pattern<N>, boundaries: Boundaries<N>): void {
50+
const [x1, y1] = pattern.anchor
51+
const [x2, y2] = pattern.target
52+
53+
boundaries.xMin = minPatternNumber(x1, x2)
54+
boundaries.xMax = maxPatternNumber(x1, x2)
55+
boundaries.yMin = minPatternNumber(y1, y2)
56+
boundaries.yMax = maxPatternNumber(y1, y2)
57+
}
58+
59+
4560
export const getBoundariesFromPattern = <N extends PatternNumber>(pattern: Pattern<N>): Boundaries<N>=> {
46-
return getBoundariesFromTwoPoints(pattern.anchor, pattern.target)
61+
const obj: Boundaries<N> = { xMin: 0 as N, xMax: 0 as N, yMin: 0 as N, yMax: 0 as N }
62+
mutateBoundariesFromPattern(pattern, obj)
63+
return obj
4764
}
4865

66+
4967
export const pointIsInBoundaries = <N extends PatternNumber>(point: Point<N>, boundaries: Boundaries<N>): boolean => {
5068
const { xMin, xMax, yMin, yMax } = boundaries
5169
const [pointX, pointY] = point
@@ -111,7 +129,7 @@ export function getRelativePatternPosition(pattern: RelativePattern, basePattern
111129
}
112130
}
113131

114-
const resolveRelativePointPosition = <N extends RelativeNumber | AbsoluteNumber>(relativePoint: RelativePoint, pattern: Pattern<N>): Point<N> => {
132+
const resolveRelativePointPosition = <N extends PatternNumber>(relativePoint: RelativePoint, pattern: Pattern<N>): Point<N> => {
115133
const [x1, y1] = pattern.anchor
116134
const [x2, y2] = pattern.target
117135
const [x, y] = relativePoint
@@ -133,7 +151,7 @@ export const getMousePoint = (
133151
return mapPointFromViewportSpace(mousePosition, getScreenSize(ctx))
134152
}
135153

136-
export function combinePatterns<ParentNumber extends RelativeNumber | AbsoluteNumber>(parent: Pattern<ParentNumber>, child: RelativePattern): Pattern<ParentNumber> {
154+
export function combinePatterns<ParentNumber extends PatternNumber>(parent: Pattern<ParentNumber>, child: RelativePattern): Pattern<ParentNumber> {
137155
return {
138156
anchor: resolveRelativePointPosition(child.anchor, parent),
139157
target: resolveRelativePointPosition(child.target, parent),

0 commit comments

Comments
 (0)