Add StyleBench
[WebKit-https.git] / PerformanceTests / StyleBench / resources / style-bench.js
1 class Random
2 {
3     constructor(seed)
4     {
5         this.seed = seed % 2147483647;
6         if (this.seed <= 0)
7             this.seed += 2147483646;
8     }
9
10     get next()
11     {
12         return this.seed = this.seed * 16807 % 2147483647;
13     }
14
15     chance(chance)
16     {
17         return this.next % 1048576 < chance * 1048576;
18     }
19
20     number(under)
21     {
22         return this.next % under;
23     }
24 }
25
26 function nextAnimationFrame()
27 {
28     return new Promise(resolve => requestAnimationFrame(resolve));
29 }
30
31 class StyleBench
32 {
33     static defaultConfiguration()
34     {
35         return {
36             name: 'Default',
37             elementTypeCount: 10,
38             elementChance: 0.5,
39             classCount: 200,
40             classChance: 0.3,
41             combinators: [' ', '>',],
42             pseudoClasses: [],
43             pseudoClassChance: 0,
44             beforeAfterChance: 0,
45             maximumSelectorLength: 6,
46             ruleCount: 5000,
47             elementCount: 20000,
48             maximumTreeDepth: 6,
49             maximumTreeWidth: 50,
50             repeatingSequenceChance: 0.2,
51             repeatingSequenceMaximumLength: 3,
52             leafClassMutationChance: 0.1,
53             styleSeed: 1,
54             domSeed: 2,
55         };
56     }
57
58     static descendantCombinatorConfiguration()
59     {
60         return Object.assign(this.defaultConfiguration(), {
61             name: 'Descendant and child combinators',
62         });
63     }
64
65     static siblingCombinatorConfiguration()
66     {
67         return Object.assign(this.defaultConfiguration(), {
68             name: 'Sibling combinators',
69             combinators: [' ', ' ', '>', '>', '~', '+',],
70         });
71     }
72
73     static pseudoClassConfiguration()
74     {
75         return Object.assign(this.defaultConfiguration(), {
76             name: 'Positional pseudo classes',
77             pseudoClassChance: 0.1,
78             pseudoClasses: [
79                 'nth-child(2n+1)',
80                 'nth-last-child(3n)',
81                 'nth-of-type(3n)',
82                 'nth-last-of-type(4n)',
83                 'first-child',
84                 'last-child',
85                 'first-of-type',
86                 'last-of-type',
87                 'only-of-type',
88             ],
89         });
90     }
91
92     static beforeAndAfterConfiguration()
93     {
94         return Object.assign(this.defaultConfiguration(), {
95             name: 'Before and after pseudo elements',
96             beforeAfterChance: 0.1,
97         });
98     }
99
100     static predefinedConfigurations()
101     {
102         return [
103             this.descendantCombinatorConfiguration(),
104             this.siblingCombinatorConfiguration(),
105             this.pseudoClassConfiguration(),
106             this.beforeAndAfterConfiguration(),
107         ];
108     }
109
110     constructor(configuration)
111     {
112         this.configuration = configuration;
113
114         this.baseStyle = document.createElement("style");
115         this.baseStyle.textContent = `
116             #testroot {
117                 font-size: 10px;
118                 line-height: 10px;
119             }
120             #testroot * {
121                 display: inline-block;
122             }
123             #testroot :empty {
124                 width:10px;
125                 height:10px;
126             }
127         `;
128         document.head.appendChild(this.baseStyle);
129
130         this.random = new Random(this.configuration.styleSeed);
131         this.makeStyle();
132
133         this.random = new Random(this.configuration.domSeed);
134         this.makeTree();
135     }
136
137     randomElementName()
138     {
139         const elementTypeCount = this.configuration.elementTypeCount;
140         return `elem${ this.random.number(elementTypeCount) }`;
141     }
142
143     randomClassName()
144     {
145         const classCount = this.configuration.classCount;
146         return `class${ this.random.number(classCount) }`;
147     }
148
149     randomClassNameFromRange(range)
150     {
151         const maximum = Math.round(range * this.configuration.classCount);
152         return `class${ this.random.number(maximum) }`;
153     }
154
155     randomCombinator()
156     {
157         const combinators = this.configuration.combinators;
158         return combinators[this.random.number(combinators.length)]
159     }
160
161     randomPseudoClass()
162     {
163         const pseudoClasses = this.configuration.pseudoClasses;
164         return pseudoClasses[this.random.number(pseudoClasses.length)]
165     }
166
167     makeSimpleSelector(index, length)
168     {
169         const isLast = index == length - 1;
170         const usePseudoClass = this.random.chance(this.configuration.pseudoClassChance) && this.configuration.pseudoClasses.length;
171         const useElement = usePseudoClass || this.random.chance(this.configuration.elementChance); // :nth-of-type etc only make sense with element
172         const useClass = !useElement || this.random.chance(this.configuration.classChance);
173         const useBeforeOrAfter = isLast && this.random.chance(this.configuration.beforeAfterChance);
174         let result = "";
175         if (useElement)
176             result += this.randomElementName();
177         if (useClass) {
178             // Use a smaller pool of class names on the left side of the selectors to create containers.
179             result += "." + this.randomClassNameFromRange((index + 1) / length);
180         }
181         if (usePseudoClass)
182             result +=  ":" + this.randomPseudoClass();
183         if (useBeforeOrAfter) {
184             if (this.random.chance(0.5))
185                 result +=  "::before";
186             else
187                 result +=  "::after";
188         }
189         return result;
190     }
191
192     makeSelector()
193     {
194         const length = this.random.number(this.configuration.maximumSelectorLength) + 1;
195         let result = this.makeSimpleSelector(0, length);
196         for (let i = 0; i < length; ++i) {
197             const combinator = this.randomCombinator();
198             if (combinator != ' ')
199                 result += " " + combinator;
200             result += " " + this.makeSimpleSelector(i, length);
201         }
202         return result;
203     }
204
205     get randomColorComponent()
206     {
207         return this.random.next % 256;
208     }
209
210     makeDeclaration(selector)
211     {
212         let declaration = `background-color: rgb(${this.randomColorComponent}, ${this.randomColorComponent}, ${this.randomColorComponent});`;
213
214         if (selector.endsWith('::before') || selector.endsWith('::after'))
215             declaration += " content: '\xa0';";
216
217         return declaration;
218     }
219
220     makeRule()
221     {
222         const selector = this.makeSelector();
223         return selector + " { " + this.makeDeclaration(selector) + " }";
224     }
225
226     makeStylesheet(size)
227     {
228         let cssText = "";
229         for (let i = 0; i < size; ++i)
230             cssText += this.makeRule() + "\n";
231         return cssText;
232     }
233
234     makeStyle()
235     {
236         this.testStyle = document.createElement("style");
237         this.testStyle.textContent = this.makeStylesheet(this.configuration.ruleCount);
238
239         document.head.appendChild(this.testStyle);
240     }
241
242     makeElement()
243     {
244         const element = document.createElement(this.randomElementName());
245         const hasClasses = this.random.chance(0.5);
246         if (hasClasses) {
247             const count = this.random.number(3) + 1;
248             for (let i = 0; i < count; ++i)
249                 element.classList.add(this.randomClassName());
250         }
251         return element;
252     }
253
254     makeTreeWithDepth(parent, remainingCount, depth)
255     {
256         const maximumDepth = this.configuration.maximumTreeDepth;
257         const maximumWidth =  this.configuration.maximumTreeWidth;
258         const nonEmptyChance = (maximumDepth - depth) / maximumDepth;
259
260         const shouldRepeat = this.random.chance(this.configuration.repeatingSequenceChance);
261         const repeatingSequenceLength = shouldRepeat ? this.random.number(this.configuration.repeatingSequenceMaximumLength) + 1 : 0;
262
263         let childCount = 0;
264         if (depth == 0)
265             childCount = remainingCount;
266         else if (this.random.chance(nonEmptyChance))
267             childCount = this.random.number(maximumWidth * depth / maximumDepth);
268
269         let repeatingSequence = [];
270         let repeatingSequenceSize = 0;
271         for (let i = 0; i < childCount; ++i) {
272             if (shouldRepeat && repeatingSequence.length == repeatingSequenceLength && repeatingSequenceSize < remainingCount) {
273                 for (const subtree of repeatingSequence)
274                     parent.appendChild(subtree.cloneNode(true));
275                 remainingCount -= repeatingSequenceSize;
276                 if (!remainingCount)
277                     return 0;
278                 continue;
279             }
280             const element = this.makeElement();
281             parent.appendChild(element);
282
283             if (!--remainingCount)
284                 return 0;
285             remainingCount = this.makeTreeWithDepth(element, remainingCount, depth + 1);
286             if (!remainingCount)
287                 return 0;
288
289             if (shouldRepeat && repeatingSequence.length < repeatingSequenceLength) {
290                 repeatingSequence.push(element);
291                 repeatingSequenceSize += element.querySelectorAll("*").length + 1;
292             }
293         }
294         return remainingCount;
295     }
296
297     makeTree()
298     {
299         this.testRoot = document.querySelector("#testroot");
300         const elementCount = this.configuration.elementCount;
301
302         this.makeTreeWithDepth(this.testRoot, elementCount, 0);
303
304         this.updateCachedTestElements();
305     }
306
307     updateCachedTestElements()
308     {
309         this.testElements = this.testRoot.querySelectorAll("*");
310     }
311
312     randomTreeElement()
313     {
314         const randomIndex = this.random.number(this.testElements.length);
315         return this.testElements[randomIndex]
316     }
317
318     addClasses(count)
319     {
320         for (let i = 0; i < count;) {
321             const element = this.randomTreeElement();
322             // There are more leaves than branches. Avoid skewing towards leaf mutations.
323             if (!element.firstChild && !this.random.chance(this.configuration.leafClassMutationChance))
324                 continue;
325             ++i;
326             const classList = element.classList;
327             classList.add(this.randomClassName());
328         }
329     }
330
331     removeClasses(count)
332     {
333         for (let i = 0; i < count;) {
334             const element = this.randomTreeElement();
335             const classList = element.classList;
336             if (!element.firstChild && !this.random.chance(this.configuration.leafClassMutationChance))
337                 continue;
338             if (!classList.length)
339                 continue;
340             ++i;
341             classList.remove(classList[0]);
342         }
343     }
344
345     addLeafElements(count)
346     {
347         for (let i = 0; i < count;) {
348             const parent = this.randomTreeElement();
349             // Avoid altering tree shape by turning many leaves into containers.
350             if (!parent.firstChild)
351                 continue;
352             ++i;
353             const children = parent.childNodes;
354             const index = this.random.number(children.length + 1);
355             parent.insertBefore(this.makeElement(), children[index]);
356         }
357         this.updateCachedTestElements();
358     }
359
360     removeLeafElements(count)
361     {
362         for (let i = 0; i < count;) {
363             const element = this.randomTreeElement();
364
365             const canRemove = !element.firstChild && element.parentNode;
366             if (!canRemove)
367                 continue;
368             ++i;
369             element.parentNode.removeChild(element);
370         }
371         this.updateCachedTestElements();
372     }
373
374     async runForever()
375     {
376         while (true) {
377             this.addClasses(10);
378             this.removeClasses(10);
379             this.addLeafElements(10);
380             this.removeLeafElements(10);
381
382             await nextAnimationFrame();
383         }
384     }
385 }