557 lines
18 KiB
JavaScript
557 lines
18 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
}
|