Skip to content

Commit 0717e93

Browse files
committed
feat: Simplify rewrite for dynamic attribute values.
This change greatly simplifies the per-element rewrite for elements that have dynamic attribute values. In the element rewrite we no longer enumerate all the attribute values as possible styles and we don't enumerate the specific values as part of the switch condition. Instead we just reference the attribute as being a switch condition and let the runtime fill in the possible values from new information that is passed in for the aggregate rewrite data. The aggregate rewrite data is more explicit than it strictly needs to be, the rewrite code could do some sort of string manipulation in order to construct the attribute value style name instead of looking it up in a table. However that solution would preclude a later optimization where we optimize away the strings and replace them with unique numbers. In production code this change reduces the rewrite significantly (by more than 30 arguments in one element I examined) and should produce a significant savings in aggregate by avoiding duplication when the same attributes are used dynamically on different template elements throughout the application.
1 parent 901032b commit 0717e93

File tree

3 files changed

+90
-80
lines changed

3 files changed

+90
-80
lines changed

packages/@css-blocks/ember-app/runtime/app/services/StyleEvaluator.ts

+24-23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ObjectDictionary } from "@opticss/util";
2+
13
import { AggregateRewriteData } from "./AggregateRewriteData";
24

35
export type ClassNameExpression = Array<string | number | boolean | null>;
@@ -41,6 +43,7 @@ export class StyleEvaluator {
4143
// because the block it implements has changed since the implementing block
4244
// was precompiled and released.
4345
blockStyleIds: Array<Array<number | null>> = [];
46+
blockGroups: Array<ObjectDictionary<ObjectDictionary<string>>> = [];
4447
styles: Array<number | null> = [];
4548
constructor(data: AggregateRewriteData, args: ClassNameExpression) {
4649
this.data = data;
@@ -60,6 +63,7 @@ export class StyleEvaluator {
6063
let styleIds = blockInfo.implementations[runtimeBlockIndex];
6164
assert(styleIds, "unknown implementation");
6265
this.blockStyleIds.push(styleIds);
66+
this.blockGroups.push(blockInfo.groups);
6367
}
6468
}
6569

@@ -99,6 +103,7 @@ export class StyleEvaluator {
99103
// to true.
100104
let numConditions = this.num();
101105
let styleStates = new Array<boolean>(this.styles.length);
106+
let stylesApplied = new Set<number>();
102107
while (numConditions--) {
103108
let condition = this.num();
104109
switch (condition) {
@@ -113,14 +118,13 @@ export class StyleEvaluator {
113118
this.evaluateTernaryCondition(styleStates);
114119
break;
115120
case Condition.switch:
116-
this.evaluateSwitchCondition(styleStates);
121+
this.evaluateSwitchCondition(stylesApplied);
117122
break;
118123
default:
119124
throw new Error(`Unknown condition type ${condition}`);
120125
}
121126
}
122127

123-
let stylesApplied = new Set<number>();
124128
for (let i = 0; i < this.styles.length; i++) {
125129
if (styleStates[i] && this.styles[i] !== null) {
126130
stylesApplied.add(this.styles[i]!);
@@ -129,33 +133,30 @@ export class StyleEvaluator {
129133

130134
return stylesApplied;
131135
}
132-
evaluateSwitchCondition(styleStates: Array<boolean>) {
136+
137+
evaluateSwitchCondition(stylesApplied: Set<number>) {
133138
let falsyBehavior = this.num();
139+
let blockIndex = this.num();
140+
let attribute = this.str();
134141
let currentValue = this.str(true, true);
135-
let numValues = this.num();
136-
let found = false;
137-
let legal: Array<String> = [];
138-
while (numValues--) {
139-
let v = this.str();
140-
legal.push(v);
141-
let match = (v === currentValue);
142-
found = found || match;
143-
let numStyles = this.num();
144-
while (numStyles--) {
145-
let s = this.num();
146-
if (match) styleStates[s] = true;
147-
}
148-
}
149-
if (!found) {
150-
if (!currentValue) {
151-
if (falsyBehavior === FalsySwitchBehavior.error) {
152-
throw new Error(`A value is required.`);
153-
}
142+
if (!currentValue) {
143+
if (currentValue === null || falsyBehavior === FalsySwitchBehavior.unset) {
144+
return;
154145
} else {
155-
throw new Error(`"${currentValue} is not a known attribute value. Expected one of: ${legal.join(", ")}`);
146+
throw new Error(`A value is required for ${attribute}.`);
156147
}
157148
}
149+
let attrValue = this.blockGroups[blockIndex][attribute][currentValue];
150+
if (attrValue === undefined) {
151+
let legal = Object.keys(this.blockGroups[blockIndex][attribute]);
152+
throw new Error(`"${currentValue} is not a known attribute value. Expected one of: ${legal.join(", ")}`);
153+
}
154+
let style = this.blockStyleIds[blockIndex][this.blockStyleIndices[blockIndex][attrValue]];
155+
if (style) {
156+
stylesApplied.add(style);
157+
}
158158
}
159+
159160
evaluateTernaryCondition(styleStates: Array<boolean>) {
160161
// Ternary supports multiple styles being enabled when true
161162
// and multiple values being enabled when false.

packages/@css-blocks/ember/src/TemplateAnalyzingRewriter.ts

+63-54
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
Attribute,
23
Block,
34
Configuration as CSSBlocksConfiguration,
45
Style,
@@ -248,68 +249,59 @@ class HelperInvocationGenerator {
248249
node.params = [];
249250
node.hash = hash();
250251
node.params.push(num(HelperInvocationGenerator.HELPER_VERSION));
251-
let { blocks, blockParams } = this.buildBlockParams(sourceAnalysis);
252+
253+
let stylesUsed = new Array<Style>();
254+
let blocksUsed = new Array<Block>();
255+
let staticParams = this.buildStaticParams(sourceAnalysis, stylesUsed);
256+
let ternaryParams = this.buildTernaryParams(sourceAnalysis, stylesUsed);
257+
let booleanParams = this.buildBooleanParams(sourceAnalysis, stylesUsed);
258+
let switchParams = this.buildSwitchParams(sourceAnalysis, blocksUsed);
259+
let styleParams = this.buildStyleParams(sourceAnalysis, blocksUsed, stylesUsed);
260+
let blockParams = this.buildBlockParams(blocksUsed);
261+
252262
node.params.push(...blockParams);
253-
let { styleIndices, styleParams } = this.buildStyleParams(sourceAnalysis, blocks);
254263
node.params.push(...styleParams);
255-
node.params.push(...this.buildStaticParams(sourceAnalysis, styleIndices));
256-
node.params.push(...this.buildTernaryParams(sourceAnalysis, styleIndices));
257-
node.params.push(...this.buildBooleanParams(sourceAnalysis, styleIndices));
258-
node.params.push(...this.buildSwitchParams(sourceAnalysis, styleIndices));
264+
node.params.push(...staticParams );
265+
node.params.push(...ternaryParams);
266+
node.params.push(...booleanParams);
267+
node.params.push(...switchParams);
259268
return node;
260269
}
261270

262-
indexOfBlock(blocks: Array<Block>, style: Style) {
263-
for (let i = 0; i < blocks.length; i++) {
264-
if (style.block === blocks[i] || style.block.isAncestorOf(blocks[i])) {
265-
return i;
266-
}
267-
}
268-
throw new Error("[internal error] Block not found.");
269-
}
270-
271-
buildBlockParams(sourceAnalysis: ElementSourceAnalysis): {blocks: Array<Block>; blockParams: Array<AST.Expression>} {
271+
buildBlockParams(blocksUsed: Array<Block>): Array<AST.Expression> {
272272
const { number: num, string: str, null: nullNode } = this.builders;
273-
let blocks = [...sourceAnalysis.blocksFound];
274273
let blockParams = new Array<AST.Expression>();
275-
blockParams.push(num(blocks.length));
276-
for (let block of blocks) {
274+
blockParams.push(num(blocksUsed.length));
275+
for (let block of blocksUsed) {
277276
blockParams.push(str(block.guid));
278277
blockParams.push(nullNode()); // this could be a block when we implement block passing
279278
}
280-
return {blocks, blockParams};
279+
return blockParams;
281280
}
282281

283-
buildStyleParams(sourceAnalysis: ElementSourceAnalysis, blocks: Array<Block>): {styleIndices: Map<Style, number>; styleParams: Array<AST.Expression>} {
282+
buildStyleParams(sourceAnalysis: ElementSourceAnalysis, blocks: Array<Block>, stylesUsed: Array<Style>): Array<AST.Expression> {
284283
const { number: num, string: str } = this.builders;
285-
let styles = [...sourceAnalysis.stylesFound];
286284
let styleParams = new Array<AST.Expression>();
287-
styleParams.push(num(styles.length));
288-
let styleIndices = new Map<Style, number>();
289-
let i = 0;
290-
for (let style of styles) {
291-
styleIndices.set(style, i++);
292-
styleParams.push(num(this.indexOfBlock(blocks, style)));
285+
styleParams.push(num(stylesUsed.length));
286+
for (let style of stylesUsed) {
287+
styleParams.push(num(blockIndex(blocks, style)));
293288
styleParams.push(str(style.asSource()));
294289
}
295290
styleParams.push(num(sourceAnalysis.size()));
296-
return {
297-
styleIndices,
298-
styleParams,
299-
};
291+
return styleParams;
300292
}
301293

302-
buildStaticParams(sourceAnalysis: ElementSourceAnalysis, styleIndices: Map<Style, number>): Array<AST.Expression> {
294+
buildStaticParams(sourceAnalysis: ElementSourceAnalysis, stylesUsed: Array<Style>): Array<AST.Expression> {
303295
const { number: num } = this.builders;
304296
let params = new Array<AST.Expression>();
305297
for (let style of sourceAnalysis.staticStyles) {
306298
params.push(num(StyleCondition.STATIC));
307-
params.push(num(styleIndices.get(style)!));
299+
params.push(num(styleIndex(stylesUsed, style)));
308300
}
309301
return params;
310302
}
311303

312-
buildTernaryParams(sourceAnalysis: ElementSourceAnalysis, styleIndices: Map<Style, number>): Array<AST.Expression> {
304+
buildTernaryParams(sourceAnalysis: ElementSourceAnalysis, stylesUsed: Array<Style>): Array<AST.Expression> {
313305
const { number: num } = this.builders;
314306
let params = new Array<AST.Expression>();
315307
for (let ternaryClass of sourceAnalysis.ternaryStyles) {
@@ -322,7 +314,7 @@ class HelperInvocationGenerator {
322314
if (isTrueCondition(ternaryClass)) {
323315
params.push(num(ternaryClass.whenTrue.length));
324316
for (let cls of ternaryClass.whenTrue) {
325-
params.push(num(styleIndices.get(cls)!));
317+
params.push(num(styleIndex(stylesUsed, cls)));
326318
}
327319
} else {
328320
// there are no classes applied if true
@@ -331,7 +323,7 @@ class HelperInvocationGenerator {
331323
if (isFalseCondition(ternaryClass)) {
332324
params.push(num(ternaryClass.whenFalse.length));
333325
for (let cls of ternaryClass.whenFalse) {
334-
params.push(num(styleIndices.get(cls)!));
326+
params.push(num(styleIndex(stylesUsed, cls)));
335327
}
336328
} else {
337329
// there are no classes applied if false
@@ -341,7 +333,7 @@ class HelperInvocationGenerator {
341333
return params;
342334
}
343335

344-
buildBooleanParams(sourceAnalysis: ElementSourceAnalysis, styleIndices: Map<Style, number>): Array<AST.Expression> {
336+
buildBooleanParams(sourceAnalysis: ElementSourceAnalysis, stylesUsed: Array<Style>): Array<AST.Expression> {
345337
const { number: num } = this.builders;
346338
let params = new Array<AST.Expression>();
347339
for (let dynamicAttr of sourceAnalysis.booleanStyles) {
@@ -353,13 +345,13 @@ class HelperInvocationGenerator {
353345
}
354346
params.push(num(dynamicAttr.value.size));
355347
for (let attr of dynamicAttr.value) {
356-
params.push(num(styleIndices.get(attr)!));
348+
params.push(num(styleIndex(stylesUsed, attr)));
357349
}
358350
}
359351
return params;
360352
}
361353

362-
buildSwitchParams(sourceAnalysis: ElementSourceAnalysis, styleIndices: Map<Style, number>): Array<AST.Expression> {
354+
buildSwitchParams(sourceAnalysis: ElementSourceAnalysis, blocksUsed: Array<Block>): Array<AST.Expression> {
363355
const { number: num, string: str } = this.builders;
364356
let params = new Array<AST.Expression>();
365357

@@ -371,24 +363,21 @@ class HelperInvocationGenerator {
371363
} else {
372364
params.push(num(FalsySwitchBehavior.unset));
373365
}
374-
params.push(this.mustacheToStringExpression(this.builders, switchStyle.stringExpression!));
375-
params.push(num(values.length));
366+
// We have to find the attribute that belongs to the most specific sub-block.
367+
let attr: Attribute | undefined;
376368
for (let value of values) {
377-
let obj = switchStyle.group[value];
378-
params.push(str(value));
379-
// If there are values provided for this conditional, they are meant to be
380-
// applied instead of the selected attribute group member.
381-
if (switchStyle.value.size) {
382-
params.push(num(switchStyle.value.size));
383-
for (let val of switchStyle.value) {
384-
params.push(num(styleIndices.get(val)!));
369+
let attrValue = switchStyle.group[value];
370+
if (!attr) {
371+
attr = attrValue.attribute;
372+
} else {
373+
if (attr.block.isAncestorOf(attrValue.block)) {
374+
attr = attrValue.attribute;
385375
}
386376
}
387-
else {
388-
params.push(num(1));
389-
params.push(num(styleIndices.get(obj)!));
390-
}
391377
}
378+
params.push(num(blockIndex(blocksUsed, attr!)));
379+
params.push(str(attr!.asSource()));
380+
params.push(this.mustacheToStringExpression(this.builders, switchStyle.stringExpression!));
392381
}
393382
return params;
394383
}
@@ -432,3 +421,23 @@ class HelperInvocationGenerator {
432421
}
433422
}
434423
}
424+
425+
function styleIndex(stylesUsed: Array<Style>, style: Style): number {
426+
let index = stylesUsed.indexOf(style);
427+
if (index >= 0) { return index; }
428+
stylesUsed.push(style);
429+
return stylesUsed.length - 1;
430+
}
431+
432+
function blockIndex(blocks: Array<Block>, style: Style | Attribute) {
433+
for (let i = 0; i < blocks.length; i++) {
434+
if (style.block === blocks[i] || style.block.isAncestorOf(blocks[i])) {
435+
return i;
436+
} else if (blocks[i].isAncestorOf(style.block)) {
437+
blocks[i] = style.block;
438+
return i;
439+
}
440+
}
441+
blocks.push(style.block);
442+
return blocks.length - 1;
443+
}

packages/@css-blocks/ember/test/template-rewrite-test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ describe("Template Rewriting", function() {
188188
minify(`
189189
<div class={{-css-blocks 0 1 "${defaultBlock.guid}" null 1 0 ":scope" 1 1 0}}>
190190
<h1 class={{-css-blocks 0 1 "${headerBlock.guid}" null 1 0 ":scope" 1 1 0}}>Hello,
191-
<span class={{-css-blocks 0 3 "${defaultBlock.guid}" null "${headerBlock.guid}" null "${typographyBlock.guid}" null 6 0 ".world" 1 ".emphasis" 2 ".underline" 0 ".world[thick]" 1 ".emphasis[style=bold]" 1 ".emphasis[style=italic]" 5 1 1 1 2 3 isWorld 1 0 0 2 (eq isThick 1) 1 3 4 1 textStyle 2 "bold" 1 4 "italic" 1 5}}>World</span>!</h1>
191+
<span class={{-css-blocks 0 3 "${headerBlock.guid}" null "${typographyBlock.guid}" null "${defaultBlock.guid}" null 4 0 ".emphasis" 1 ".underline" 2 ".world" 2 ".world[thick]" 5 1 0 1 1 3 isWorld 1 2 0 2 (eq isThick 1) 1 3 4 1 0 ".emphasis[style]" textStyle}}>World</span>!</h1>
192192
<div class={{-css-blocks 0 1 "${defaultBlock.guid}" null 2 0 ".world" 0 ".planet" 1 3 isWorld 1 0 1 1}}>World</div>
193193
<div class={{-css-blocks 0 1 "${defaultBlock.guid}" null 2 0 ".planet" 0 ".world" 1 3 isWorld 1 0 1 1}}>World</div>
194194
<div class={{-css-blocks 0 1 "${defaultBlock.guid}" null 1 0 ".world" 1 3 isWorld 0 1 0}}>World</div>
@@ -334,7 +334,7 @@ describe("Template Rewriting", function() {
334334
minify(`
335335
<div class={{-css-blocks 0 1 "${defaultBlock.guid}" null 1 0 ":scope" 1 1 0}}>
336336
<h1 class={{-css-blocks 0 1 "${headerBlock.guid}" null 1 0 ":scope" 1 1 0}}>Hello,
337-
<World cssClass={{-css-blocks 0 2 "${defaultBlock.guid}" null "${headerBlock.guid}" null 5 0 ".world" 1 ".emphasis" 0 ".world[thick]" 1 ".emphasis[style=bold]" 1 ".emphasis[style=italic]" 4 1 0 1 1 1 2 4 1 (textStyle) 2 "bold" 1 3 "italic" 1 4}} />!</h1>
337+
<World cssClass={{-css-blocks 0 2 "${headerBlock.guid}" null "${defaultBlock.guid}" null 3 1 ".world" 0 ".emphasis" 1 ".world[thick]" 4 1 0 1 1 1 2 4 1 0 ".emphasis[style]" (textStyle)}} />!</h1>
338338
</div>
339339
`));
340340
let analysis = result.analysis.serialize();
@@ -400,7 +400,7 @@ describe("Template Rewriting", function() {
400400
<h1 class={{-css-blocks 0 1 "${headerBlock.guid}" null 1 0 ":scope" 1 1 0}}>Hello,
401401
{{yield (hash
402402
classnames=(hash
403-
action=(-css-blocks 0 2 "${defaultBlock.guid}" null "${headerBlock.guid}" null 5 0 ".world" 1 ".emphasis" 0 ".world[thick]" 1 ".emphasis[style=bold]" 1 ".emphasis[style=italic]" 4 1 0 1 1 2 isThick 1 2 4 1 (textStyle) 2 "bold" 1 3 "italic" 1 4)))}}
403+
action=(-css-blocks 0 2 "${headerBlock.guid}" null "${defaultBlock.guid}" null 3 1 ".world" 0 ".emphasis" 1 ".world[thick]" 4 1 0 1 1 2 isThick 1 2 4 1 0 ".emphasis[style]" (textStyle))))}}
404404
</h1>
405405
</div>
406406
`));

0 commit comments

Comments
 (0)