| class Random |
| { |
| constructor(seed) |
| { |
| this.seed = seed % 2147483647; |
| if (this.seed <= 0) |
| this.seed += 2147483646; |
| } |
| |
| get next() |
| { |
| return this.seed = this.seed * 16807 % 2147483647; |
| } |
| |
| underOne() |
| { |
| return (this.next % 1048576) / 1048576; |
| } |
| |
| chance(chance) |
| { |
| if (!chance) |
| return false; |
| return this.underOne() < chance; |
| } |
| |
| number(under) |
| { |
| return this.next % under; |
| } |
| |
| numberSquareWeightedToLow(under) |
| { |
| const random = this.underOne(); |
| const random2 = random * random; |
| return Math.floor(random2 * under); |
| } |
| } |
| |
| function nextAnimationFrame() |
| { |
| return new Promise(resolve => requestAnimationFrame(resolve)); |
| } |
| |
| class StyleBench |
| { |
| static defaultConfiguration() |
| { |
| return { |
| name: 'Default', |
| elementTypeCount: 10, |
| idChance: 0.05, |
| elementChance: 0.5, |
| classCount: 200, |
| classChance: 0.3, |
| starChance: 0.05, |
| attributeChance: 0.02, |
| attributeCount: 10, |
| attributeValueCount: 20, |
| attributeOperators: ['','='], |
| elementClassChance: 0.5, |
| elementMaximumClasses: 3, |
| elementAttributeChance: 0.2, |
| elementMaximumAttributes: 3, |
| combinators: [' ', '>',], |
| pseudoClasses: [], |
| pseudoClassChance: 0, |
| beforeAfterChance: 0, |
| maximumSelectorLength: 6, |
| ruleCount: 5000, |
| elementCount: 20000, |
| maximumTreeDepth: 6, |
| maximumTreeWidth: 50, |
| repeatingSequenceChance: 0.2, |
| repeatingSequenceMaximumLength: 3, |
| leafMutationChance: 0.1, |
| mediaQueryChance: 0, |
| mediaQueryCloseChance: 0, |
| styleSeed: 1, |
| domSeed: 2, |
| stepCount: 5, |
| isResizeTest: false, |
| mutationsPerStep: 100, |
| }; |
| } |
| |
| static descendantCombinatorConfiguration() |
| { |
| return Object.assign(this.defaultConfiguration(), { |
| name: 'Descendant and child combinators', |
| }); |
| } |
| |
| static siblingCombinatorConfiguration() |
| { |
| return Object.assign(this.defaultConfiguration(), { |
| name: 'Sibling combinators', |
| combinators: [' ', ' ', '>', '>', '~', '+',], |
| }); |
| } |
| |
| static structuralPseudoClassConfiguration() |
| { |
| return Object.assign(this.defaultConfiguration(), { |
| name: 'Structural pseudo classes', |
| pseudoClassChance: 0.1, |
| pseudoClasses: [ |
| 'first-child', |
| 'last-child', |
| 'first-of-type', |
| 'last-of-type', |
| 'only-of-type', |
| 'empty', |
| ], |
| }); |
| } |
| |
| static nthPseudoClassConfiguration() |
| { |
| return Object.assign(this.defaultConfiguration(), { |
| name: 'Nth pseudo classes', |
| pseudoClassChance: 0.1, |
| pseudoClasses: [ |
| 'nth-child(2n+1)', |
| 'nth-last-child(3n)', |
| 'nth-of-type(3n)', |
| 'nth-last-of-type(4n)', |
| ], |
| }); |
| } |
| |
| static beforeAndAfterConfiguration() |
| { |
| return Object.assign(this.defaultConfiguration(), { |
| name: 'Before and after pseudo elements', |
| beforeAfterChance: 0.1, |
| }); |
| } |
| |
| static mediaQueryConfiguration() |
| { |
| return Object.assign(this.defaultConfiguration(), { |
| name: 'Dynamic media queries', |
| isResizeTest : true, |
| mediaQueryChance: 0.01, |
| mediaQueryCloseChance: 0.3, |
| starChance: 0, |
| elementCount: 5000, |
| }); |
| } |
| |
| static predefinedConfigurations() |
| { |
| return [ |
| this.descendantCombinatorConfiguration(), |
| this.siblingCombinatorConfiguration(), |
| this.structuralPseudoClassConfiguration(), |
| this.nthPseudoClassConfiguration(), |
| this.beforeAndAfterConfiguration(), |
| this.mediaQueryConfiguration(), |
| ]; |
| } |
| |
| constructor(configuration) |
| { |
| this.configuration = configuration; |
| this.idCount = 0; |
| |
| this.baseStyle = document.createElement("style"); |
| this.baseStyle.textContent = ` |
| #testroot { |
| font-size: 10px; |
| line-height: 10px; |
| } |
| #testroot * { |
| display: inline-block; |
| height:10px; |
| min-width:10px; |
| } |
| `; |
| document.head.appendChild(this.baseStyle); |
| |
| this.random = new Random(this.configuration.styleSeed); |
| this.makeStyle(); |
| |
| this.random = new Random(this.configuration.domSeed); |
| this.makeTree(); |
| } |
| |
| randomElementName() |
| { |
| const elementTypeCount = this.configuration.elementTypeCount; |
| return `elem${ this.random.numberSquareWeightedToLow(elementTypeCount) }`; |
| } |
| |
| randomClassName() |
| { |
| const classCount = this.configuration.classCount; |
| return `class${ this.random.numberSquareWeightedToLow(classCount) }`; |
| } |
| |
| randomClassNameFromRange(range) |
| { |
| const maximum = Math.round(range * this.configuration.classCount); |
| return `class${ this.random.numberSquareWeightedToLow(maximum) }`; |
| } |
| |
| randomAttributeName() |
| { |
| const attributeCount = this.configuration.attributeCount; |
| return `attr${ this.random.numberSquareWeightedToLow(attributeCount) }`; |
| } |
| |
| randomAttributeValue() |
| { |
| const attributeValueCount = this.configuration.attributeValueCount; |
| const valueNum = this.random.numberSquareWeightedToLow(attributeValueCount); |
| if (valueNum == 0) |
| return ""; |
| if (valueNum == 1) |
| return "val"; |
| return `val${valueNum}`; |
| } |
| |
| randomCombinator() |
| { |
| const combinators = this.configuration.combinators; |
| return combinators[this.random.number(combinators.length)] |
| } |
| |
| randomPseudoClass(isLast) |
| { |
| const pseudoClasses = this.configuration.pseudoClasses; |
| const pseudoClass = pseudoClasses[this.random.number(pseudoClasses.length)] |
| if (!isLast && pseudoClass == 'empty') |
| return this.randomPseudoClass(isLast); |
| return pseudoClass; |
| } |
| |
| randomId() |
| { |
| const idCount = this.configuration.idChance * this.configuration.elementCount ; |
| return `id${ this.random.number(idCount) }`; |
| } |
| |
| randomAttributeSelector() |
| { |
| const name = this.randomAttributeName(); |
| const operators = this.configuration.attributeOperators; |
| const operator = operators[this.random.numberSquareWeightedToLow(operators.length)]; |
| if (operator == '') |
| return `[${name}]`; |
| const value = this.randomAttributeValue(); |
| return `[${name}${operator}"${value}"]`; |
| } |
| |
| makeCompoundSelector(index, length) |
| { |
| const isFirst = index == 0; |
| const isLast = index == length - 1; |
| const usePseudoClass = this.random.chance(this.configuration.pseudoClassChance) && this.configuration.pseudoClasses.length; |
| const useId = isFirst && this.random.chance(this.configuration.idChance); |
| const useElement = !useId && (usePseudoClass || this.random.chance(this.configuration.elementChance)); // :nth-of-type etc only make sense with element |
| const useAttribute = !useId && this.random.chance(this.configuration.attributeChance); |
| const useIdElementOrAttribute = useId || useElement || useAttribute; |
| const useStar = !useIdElementOrAttribute && !isFirst && this.random.chance(this.configuration.starChance); |
| const useClass = !useId && !useStar && (!useIdElementOrAttribute || this.random.chance(this.configuration.classChance)); |
| const useBeforeOrAfter = isLast && this.random.chance(this.configuration.beforeAfterChance); |
| let result = ""; |
| if (useElement) |
| result += this.randomElementName(); |
| if (useStar) |
| result = "*"; |
| if (useId) |
| result += "#" + this.randomId(); |
| if (useClass) { |
| const classCount = this.random.numberSquareWeightedToLow(2) + 1; |
| for (let i = 0; i < classCount; ++i) { |
| // Use a smaller pool of class names on the left side of the selectors to create containers. |
| result += "." + this.randomClassNameFromRange((index + 1) / length); |
| } |
| } |
| if (useAttribute) |
| result += this.randomAttributeSelector(); |
| |
| if (usePseudoClass) |
| result += ":" + this.randomPseudoClass(isLast); |
| if (useBeforeOrAfter) { |
| if (this.random.chance(0.5)) |
| result += "::before"; |
| else |
| result += "::after"; |
| } |
| return result; |
| } |
| |
| makeSelector() |
| { |
| const length = this.random.number(this.configuration.maximumSelectorLength) + 1; |
| let result = this.makeCompoundSelector(0, length); |
| for (let i = 1; i < length; ++i) { |
| const combinator = this.randomCombinator(); |
| if (combinator != ' ') |
| result += " " + combinator; |
| result += " " + this.makeCompoundSelector(i, length); |
| } |
| return result; |
| } |
| |
| get randomColorComponent() |
| { |
| return this.random.next % 256; |
| } |
| |
| makeDeclaration(selector) |
| { |
| let declaration = `background-color: rgb(${this.randomColorComponent}, ${this.randomColorComponent}, ${this.randomColorComponent});`; |
| |
| if (selector.endsWith('::before') || selector.endsWith('::after')) |
| declaration += " content: ''; min-width:5px; display:inline-block;"; |
| |
| return declaration; |
| } |
| |
| makeRule() |
| { |
| const selector = this.makeSelector(); |
| return selector + " { " + this.makeDeclaration(selector) + " }"; |
| } |
| |
| makeMediaQuery() |
| { |
| let width = this.random.number(500); |
| width = 300 + width - (width % 100); |
| if (this.random.chance(0.5)) |
| return `@media (min-width: ${width}px) {`; |
| return `@media (max-width: ${width}px) {`; |
| } |
| |
| makeStylesheet(size) |
| { |
| let cssText = ""; |
| |
| let inMediaQuery = false; |
| for (let i = 0; i < size; ++i) { |
| if (!inMediaQuery && this.random.chance(this.configuration.mediaQueryChance)) { |
| cssText += this.makeMediaQuery() + "\n";; |
| inMediaQuery = true; |
| } |
| |
| cssText += this.makeRule() + "\n"; |
| |
| if (inMediaQuery && this.random.chance(this.configuration.mediaQueryCloseChance)) { |
| cssText += "}\n"; |
| inMediaQuery = false; |
| } |
| } |
| return cssText; |
| } |
| |
| makeStyle() |
| { |
| this.testStyle = document.createElement("style"); |
| this.testStyle.textContent = this.makeStylesheet(this.configuration.ruleCount); |
| |
| document.head.appendChild(this.testStyle); |
| } |
| |
| makeElement() |
| { |
| const element = document.createElement(this.randomElementName()); |
| const hasClasses = this.random.chance(this.configuration.elementClassChance); |
| const hasAttributes = this.random.chance(this.configuration.elementAttributeChance); |
| if (hasClasses) { |
| const count = this.random.numberSquareWeightedToLow(this.configuration.elementMaximumClasses) + 1; |
| for (let i = 0; i < count; ++i) |
| element.classList.add(this.randomClassName()); |
| } |
| if (hasAttributes) { |
| const count = this.random.number(this.configuration.elementMaximumAttributes) + 1; |
| for (let i = 0; i < count; ++i) |
| element.setAttribute(this.randomAttributeName(), this.randomAttributeValue()); |
| } |
| const hasId = this.random.chance(this.configuration.idChance); |
| if (hasId) { |
| element.id = `id${ this.idCount }`; |
| this.idCount++; |
| } |
| return element; |
| } |
| |
| makeTreeWithDepth(parent, remainingCount, depth) |
| { |
| const maximumDepth = this.configuration.maximumTreeDepth; |
| const maximumWidth = this.configuration.maximumTreeWidth; |
| const nonEmptyChance = (maximumDepth - depth) / maximumDepth; |
| |
| const shouldRepeat = this.random.chance(this.configuration.repeatingSequenceChance); |
| const repeatingSequenceLength = shouldRepeat ? this.random.number(this.configuration.repeatingSequenceMaximumLength) + 1 : 0; |
| |
| let childCount = 0; |
| if (depth == 0) |
| childCount = remainingCount; |
| else if (this.random.chance(nonEmptyChance)) |
| childCount = this.random.number(maximumWidth * depth / maximumDepth); |
| |
| let repeatingSequence = []; |
| let repeatingSequenceSize = 0; |
| for (let i = 0; i < childCount; ++i) { |
| if (shouldRepeat && repeatingSequence.length == repeatingSequenceLength && repeatingSequenceSize < remainingCount) { |
| for (const subtree of repeatingSequence) |
| parent.appendChild(subtree.cloneNode(true)); |
| remainingCount -= repeatingSequenceSize; |
| if (!remainingCount) |
| return 0; |
| continue; |
| } |
| const element = this.makeElement(); |
| parent.appendChild(element); |
| |
| if (!--remainingCount) |
| return 0; |
| remainingCount = this.makeTreeWithDepth(element, remainingCount, depth + 1); |
| if (!remainingCount) |
| return 0; |
| |
| if (shouldRepeat && repeatingSequence.length < repeatingSequenceLength) { |
| repeatingSequence.push(element); |
| repeatingSequenceSize += element.querySelectorAll("*").length + 1; |
| } |
| } |
| return remainingCount; |
| } |
| |
| makeTree() |
| { |
| this.testRoot = document.querySelector("#testroot"); |
| const elementCount = this.configuration.elementCount; |
| |
| this.makeTreeWithDepth(this.testRoot, elementCount, 0); |
| |
| this.updateCachedTestElements(); |
| } |
| |
| updateCachedTestElements() |
| { |
| this.testElements = this.testRoot.querySelectorAll("*"); |
| } |
| |
| randomTreeElement() |
| { |
| const randomIndex = this.random.number(this.testElements.length); |
| return this.testElements[randomIndex] |
| } |
| |
| addClasses(count) |
| { |
| for (let i = 0; i < count;) { |
| const element = this.randomTreeElement(); |
| // There are more leaves than branches. Avoid skewing towards leaf mutations. |
| if (!element.firstChild && !this.random.chance(this.configuration.leafMutationChance)) |
| continue; |
| ++i; |
| const classList = element.classList; |
| classList.add(this.randomClassName()); |
| } |
| } |
| |
| removeClasses(count) |
| { |
| for (let i = 0; i < count;) { |
| const element = this.randomTreeElement(); |
| const classList = element.classList; |
| if (!element.firstChild && !this.random.chance(this.configuration.leafMutationChance)) |
| continue; |
| if (!classList.length) |
| continue; |
| ++i; |
| classList.remove(classList[0]); |
| } |
| } |
| |
| addLeafElements(count) |
| { |
| for (let i = 0; i < count;) { |
| const parent = this.randomTreeElement(); |
| // Avoid altering tree shape by turning many leaves into containers. |
| if (!parent.firstChild) |
| continue; |
| ++i; |
| const children = parent.childNodes; |
| const index = this.random.number(children.length + 1); |
| parent.insertBefore(this.makeElement(), children[index]); |
| } |
| this.updateCachedTestElements(); |
| } |
| |
| removeLeafElements(count) |
| { |
| for (let i = 0; i < count;) { |
| const element = this.randomTreeElement(); |
| |
| const canRemove = !element.firstChild && element.parentNode; |
| if (!canRemove) |
| continue; |
| ++i; |
| element.parentNode.removeChild(element); |
| } |
| this.updateCachedTestElements(); |
| } |
| |
| mutateAttributes(count) |
| { |
| for (let i = 0; i < count;) { |
| const element = this.randomTreeElement(); |
| // There are more leaves than branches. Avoid skewing towards leaf mutations. |
| if (!element.firstChild && !this.random.chance(this.configuration.leafMutationChance)) |
| continue; |
| const attributeNames = element.getAttributeNames(); |
| let mutatedAttributes = false; |
| for (const name of attributeNames) { |
| if (name == "class" || name == "id") |
| continue; |
| if (this.random.chance(0.5)) |
| element.removeAttribute(name); |
| else |
| element.setAttribute(name, this.randomAttributeValue()); |
| mutatedAttributes = true; |
| } |
| if (!mutatedAttributes) { |
| const attributeCount = this.random.number(this.configuration.elementMaximumAttributes) + 1; |
| for (let j = 0; j < attributeCount; ++j) |
| element.setAttribute(this.randomAttributeName(), this.randomAttributeValue()); |
| } |
| ++i; |
| } |
| } |
| |
| resizeViewToWidth(width) |
| { |
| window.frameElement.style.width = width + "px"; |
| } |
| |
| async runForever() |
| { |
| while (true) { |
| this.addClasses(10); |
| this.removeClasses(10); |
| this.addLeafElements(10); |
| this.removeLeafElements(10); |
| this.mutateAttributes(10); |
| |
| await nextAnimationFrame(); |
| } |
| } |
| } |