<commit_before>function StackedAreaChart({
	xSeries,
	marginTop = 30, // top margin, in pixels
	marginRight = 100, // right margin, in pixels
	marginBottom = 60, // bottom margin, in pixels
	marginLeft = 50, // left margin, in pixels
} = {}) {
	const width = 756;
	const height = 189;
	const svg = d3.create("svg")
		.attr("preserveAspectRatio", "xMinYMin meet")
		.attr("viewBox", [0, 0, width, height]);

	const xRange = [marginLeft, width - marginRight]; // [left, right]
	const yRange = [height - marginBottom, marginTop]; // [bottom, top]

	// Add empty axes first.

	svg.append("g")
		.classed("x axis", true)
		.attr("transform", `translate(0,${height-marginBottom})`)

	svg.append("g")
		.classed("y axis", true)
		.attr("transform", `translate(${marginLeft},0)`)

	const update = function(data, mutTime) {
		const seriesKeys = Object.keys(data[0]).filter(s => s !== xSeries);

		let seriesColors = new Array();
		if (seriesKeys.length > 3) {
			const colorFn = d3.interpolateViridis; 
			for (let i = 0; i < seriesKeys.length; i++) {
				seriesColors.push(colorFn((i / 10) - Math.floor(i/10)));
			}
		} else {
			seriesColors = ["#253443", "#007d9c", "#50b7e0"];
			if (seriesKeys.length < 3) {
				seriesColors = seriesColors.slice(seriesKeys.length-1);
			}
		}
		const seriesScale = d3.scaleOrdinal()
			.domain(seriesKeys)
			.range(seriesColors);

		const yStack = (d3.stack().keys(seriesKeys))(data);

		const xDomain = d3.extent(d3.map(data, p => p[xSeries]));
		const yDomain = d3.extent(d3.map(yStack[yStack.length-1], p => p[1]));
		yDomain[0] = 0;

		const xScale = d3.scaleLinear(xDomain, xRange);
		const yScale = d3.scaleLinear(yDomain, yRange);

		const xAxis = d3.axisBottom(xScale).tickFormat(x => `${x.toFixed(1)} s`);
		svg.selectAll("g.x.axis")
			.call(xAxis);

		const yAxis = d3.axisLeft(yScale).ticks(5).tickFormat(x => `${x.toFixed(0)} MiB`);
		svg.selectAll("g.y.axis")
			.call(yAxis)

		const area = d3.area()
			.curve(d3.curveLinear)
			.x(d => xScale(d.data[xSeries]))
			.y0(d => yScale(d[0]))
			.y1(d => yScale(d[1]));

		svg.selectAll("path.series")
			.data(yStack)
			.join("path")
				.classed("series", true)
				.attr("d", area)
				.style("fill", d => seriesScale(d.key));

		svg.selectAll("text.label")
			.data(seriesKeys)
			.join("text")
				.classed("label", true)
				.attr("text-anchor", "left")
				.attr("font-size", "12px")
				.attr("x", width-marginRight+20)
				.attr("y", d => (seriesKeys.length-1-seriesKeys.indexOf(d))*24+60)
				.attr("fill", "currentColor")
				.attr("display", (() => {
					if (seriesKeys.length <= 3) {
						return "inherit";
					}
					return "none";
				})())
				.text(d => d);

		svg.selectAll("rect.legend")
			.data(seriesKeys)
			.join("rect")
				.classed("legend", true)
				.attr("stroke", "none")
				.attr("x", width-marginRight+7)
				.attr("y", d => (seriesKeys.length-1-seriesKeys.indexOf(d))*24+51)
				.attr("width", 10)
				.attr("height", 10)
				.attr("display", (() => {
					if (seriesKeys.length <= 3) {
						return "inherit";
					}
					return "none";
				})())
				.attr("fill", d => seriesScale(d));

		svg.selectAll("text.duration")
			.data([xDomain[1]])
			.join("text")
				.classed("duration", true)
				.attr("text-anchor", "left")
				.attr("font-size", "10px")
				.attr("x", width-marginRight+5)
				.attr("y", height-marginBottom+10)
				.attr("fill", "currentColor")
				.attr("font-weight", "bold")
				.text(d => `Total: ${d.toFixed(2)} s`);

		svg.selectAll("text.results")
			.data([[(xDomain[1]-mutTime)/xDomain[1]*100, yDomain[1]]])
			.join("text")
				.classed("results", true)
				.attr("text-anchor", "middle")
				.attr("font-size", "12px")
				.attr("x", marginLeft + (width-marginLeft-marginRight)/2)
				.attr("y", height-marginBottom+37)
				.attr("fill", "currentColor")
				.attr("font-weight", "bold")
				.text(d => `GC CPU = ${d[0].toFixed(1)}%, Peak Mem = ${d[1].toFixed(1)} MiB`);

		const peakLive = d3.max(d3.map(data, p => p["Live Heap"]));
		const otherMem = d3.max(d3.map(data, p => p["Other Mem."]));

		svg.selectAll("text.subresults")
			.data([[peakLive, otherMem]])
			.join("text")
				.classed("subresults", true)
				.attr("text-anchor", "middle")
				.attr("font-size", "11px")
				.attr("x", marginLeft + (width-marginLeft-marginRight)/2)
				.attr("y", height-marginBottom+51)
				.attr("fill", "currentColor")
				.text(d => {
					let base = "";
					if (d[0]) {
						base += `Peak Live Mem = ${d[0].toFixed(1)} MiB`;
					}
					if (d[1]) {
						base += `, Other Mem = ${d[1].toFixed(1)} MiB`;
					}
					if (base !== "") {
						base = "(" + base + ")";
					}
					return base;
				});
	}
	return [svg.node(), update];
}

function gcModel(workload, config) {
	let otherMem = config["otherMem"];
	if (typeof(otherMem) !== 'number') {
		otherMem = document.getElementById(config["otherMem"]).value;
	}
	let gogc = config["GOGC"];
	if (typeof(gogc) !== 'number') {
		gogc = document.getElementById(config["GOGC"]).value;
	}
	let memoryLimit = config["memoryLimit"];
	if (typeof(memoryLimit) !== 'number') {
		memoryLimit = document.getElementById(config["memoryLimit"]).value;
	}
	let initialLive = 0;
	if ("initialLive" in config) {
		initialLive = config["initialLive"];
	}
	let trackLive = false;
	if ("trackLive" in config) {
		trackLive = config["trackLive"];
		if (typeof(trackLive) !== 'boolean') {
			trackLive = document.getElementById(config["trackLive"]).checked;
		}
	}
	let fixedWindow = Infinity;
	if ("fixedWindow" in config) {
		fixedWindow = config["fixedWindow"];
	}
	const data = new Array();

	// State.
	const minHeapGoal = 4; // MiB
	let t = 0;
	let liveHeap = initialLive;
	let newHeap = 0;
	let liveFromCycle = new Array();
	liveFromCycle.push(initialLive);
	liveFromCycle.push(0);

	const computeHeapGoal = (liveHeap) => {
		let heapGoal = liveHeap*(1.0 + (gogc / 100));
		if (gogc === Infinity) {
			heapGoal = Infinity;
		}
		if (heapGoal+otherMem > memoryLimit) {
			heapGoal = memoryLimit - otherMem
		}
		if (gogc !== Infinity && heapGoal < minHeapGoal) {
			heapGoal = minHeapGoal
		}
		if (heapGoal < liveHeap + 0.0625) {
			heapGoal = liveHeap + 0.0625
		}
		return heapGoal
	}
	let heapGoal = computeHeapGoal(minHeapGoal / (1 + gogc/100)); // Fake a live heap for minHeapGoal.
	if (initialLive !== 0) {
		heapGoal = computeHeapGoal(initialLive);
	}

	let n = 0;
	const emit = function() {
		const datum = {"t": t};
		// The series will be automatically stacked, so for the best
		// possible presentation, we should make sure to put in
		// "other mem" first, then "live," then "new."
		// This is roughly in order of "least dynamic" series
		// to "most dynamic" which helps make the graph easier to
		// interpret.
		if (otherMem !== 0) {
			datum["Other Mem."] = otherMem;
		}
		if (trackLive) {
			for (let i = 0; i < liveFromCycle.length; i++) {
				datum[`Live Heap From GC ${i+1}`] = liveFromCycle[i];
			}
		} else {
			datum["Live Heap"] = liveHeap;
			datum["New Heap"] = newHeap;
		}
		data.push(datum)
	}

	// Emit points.
	emit();
	let nextLive = 0;
	let nextWillLive = 0;
	let nextWillDie = 0;
	let totalMutTime = 0;
	for (const work of workload) {
		let left = work.duration;
		let lastLive = liveHeap + nextLive;
		const willLive = work.duration * work.allocRate * work.newSurvivalRate;
		const willDie = lastLive * work.oldDeathRate;
		while (left > 0) {
			if (t >= fixedWindow) {
				break;
			} else if (t + left > fixedWindow) {
				left = fixedWindow - t;
			}
			let alloc = left * work.allocRate;
			let endCycle = false;
			if (liveHeap+newHeap+alloc > heapGoal) {
				alloc = heapGoal-liveHeap-newHeap;
				endCycle = true;
			}
			newHeap += alloc;

			// Calculate mutator time.
			const mutTime = alloc / work.allocRate;
			left -= mutTime;
			t += mutTime;
			totalMutTime += mutTime;
			nextLive += (willLive - willDie) * (mutTime / work.duration);

			// For tracking per-GC live memory.
			nextWillLive += willLive * (mutTime / work.duration);
			nextWillDie += willDie * (mutTime / work.duration);
			liveFromCycle[liveFromCycle.length-1] = newHeap;

			if (endCycle) {
				emit();

				liveHeap += nextLive;
				for (let i = 0; i < liveFromCycle.length; i++) {
					const live = liveFromCycle[i];
					if (live > 0) {
						if (live > nextWillDie) {
							liveFromCycle[i] -= nextWillDie;
							nextWillDie = 0;
							break;
						}
						nextWillDie -= live;
						liveFromCycle[i] = 0;
					}
				}
				liveFromCycle[liveFromCycle.length-1] = nextWillLive;

				nextLive = 0;
				nextWillLive = 0;
				nextWillDie = 0;
				newHeap = 0;
				const gcTime = liveHeap / work.scanRate + config.fixedCost;
				t += gcTime;

				emit();

				heapGoal = computeHeapGoal(liveHeap)

				liveFromCycle.push(newHeap);
			}
		}
		emit();
	}
	if (trackLive) {
		for (let i = 0; i < data.length; i++) {
			for (let j = 0; j < liveFromCycle.length; j++) {
				const key = `Live Heap From GC ${j+1}`;
				if (!(key in data[i])) {
					data[i][key] = 0;
				}
			}
		}
	}
	return [data, totalMutTime];
}

const graphs = document.querySelectorAll('.gc-guide-graph');

for (let i = 0; i < graphs.length; i++) {
	const workload = JSON.parse(graphs[i].getAttribute("data-workload"));
	const config = JSON.parse(graphs[i].getAttribute("data-config"));
	const [chart, update] = StackedAreaChart({xSeries: "t"});

	const setupSlider = function(parameter, f, fmt) {
		if (typeof(config[parameter]) !== 'number') {
			const id = config[parameter];
			const slider = document.getElementById(id);
			const display = document.getElementById(id+"-display");
			const value = f(slider.value);

			if (display) {
				display.innerHTML = fmt(value);
			}
			config[parameter] = value;

			slider.oninput = function() {
				const value = f(this.value);

				if (display) {
					display.innerHTML = fmt(value);
				}
				config[parameter] = value;

				const [data, mutTime] = gcModel(workload, config);
				update(data, mutTime);
			}
		}
	};
	const setupCheckbox = function(parameter) {
		if (parameter in config && typeof(config[parameter]) !== 'boolean') {
			const id = config[parameter];
			const checkbox = document.getElementById(id);

			config[parameter] = checkbox.checked;

			checkbox.oninput = function() {
				config[parameter] = checkbox.checked;

				const [data, mutTime] = gcModel(workload, config);
				update(data, mutTime);
			}
		}
	};
	setupSlider("otherMem", x => parseInt(x), x => `${x} MiB`);
	setupSlider("GOGC", x => {
		const v = Math.round(Math.pow(2, parseFloat(x)))
		if (v >= 1024) {
			return Infinity;
		}
		return v;
	}, x => {
		if (x === Infinity) {
			return "off";
		}
		return `${x}`;
	});
	setupSlider("memoryLimit", x => parseFloat(x), x => `${x.toFixed(1)} MiB`);
	setupCheckbox("trackLive");

	const [data, mutTime] = gcModel(workload, config);
	update(data, mutTime);
	graphs[i].appendChild(chart);
}
<commit_msg>_content/doc/gc-guide: explicitly set font size for axis labels<commit_after>function StackedAreaChart({
	xSeries,
	marginTop = 30, // top margin, in pixels
	marginRight = 100, // right margin, in pixels
	marginBottom = 60, // bottom margin, in pixels
	marginLeft = 50, // left margin, in pixels
} = {}) {
	const width = 756;
	const height = 189;
	const svg = d3.create("svg")
		.attr("preserveAspectRatio", "xMinYMin meet")
		.attr("viewBox", [0, 0, width, height]);

	const xRange = [marginLeft, width - marginRight]; // [left, right]
	const yRange = [height - marginBottom, marginTop]; // [bottom, top]

	// Add empty axes first.

	svg.append("g")
		.classed("x axis", true)
		.attr("transform", `translate(0,${height-marginBottom})`)

	svg.append("g")
		.classed("y axis", true)
		.attr("transform", `translate(${marginLeft},0)`)

	const update = function(data, mutTime) {
		const seriesKeys = Object.keys(data[0]).filter(s => s !== xSeries);

		let seriesColors = new Array();
		if (seriesKeys.length > 3) {
			const colorFn = d3.interpolateViridis;
			for (let i = 0; i < seriesKeys.length; i++) {
				seriesColors.push(colorFn((i / 10) - Math.floor(i/10)));
			}
		} else {
			seriesColors = ["#253443", "#007d9c", "#50b7e0"];
			if (seriesKeys.length < 3) {
				seriesColors = seriesColors.slice(seriesKeys.length-1);
			}
		}
		const seriesScale = d3.scaleOrdinal()
			.domain(seriesKeys)
			.range(seriesColors);

		const yStack = (d3.stack().keys(seriesKeys))(data);

		const xDomain = d3.extent(d3.map(data, p => p[xSeries]));
		const yDomain = d3.extent(d3.map(yStack[yStack.length-1], p => p[1]));
		yDomain[0] = 0;

		const xScale = d3.scaleLinear(xDomain, xRange);
		const yScale = d3.scaleLinear(yDomain, yRange);

		const xAxis = d3.axisBottom(xScale).tickFormat(x => `${x.toFixed(1)} s`);
		svg.selectAll("g.x.axis")
			.style("font-size", "11px")
			.call(xAxis);

		const yAxis = d3.axisLeft(yScale).ticks(5).tickFormat(x => `${x.toFixed(0)} MiB`);
		svg.selectAll("g.y.axis")
			.style("font-size", "11px")
			.call(yAxis);

		const area = d3.area()
			.curve(d3.curveLinear)
			.x(d => xScale(d.data[xSeries]))
			.y0(d => yScale(d[0]))
			.y1(d => yScale(d[1]));

		svg.selectAll("path.series")
			.data(yStack)
			.join("path")
				.classed("series", true)
				.attr("d", area)
				.style("fill", d => seriesScale(d.key));

		svg.selectAll("text.label")
			.data(seriesKeys)
			.join("text")
				.classed("label", true)
				.attr("text-anchor", "left")
				.attr("font-size", "12px")
				.attr("x", width-marginRight+20)
				.attr("y", d => (seriesKeys.length-1-seriesKeys.indexOf(d))*24+60)
				.attr("fill", "currentColor")
				.attr("display", (() => {
					if (seriesKeys.length <= 3) {
						return "inherit";
					}
					return "none";
				})())
				.text(d => d);

		svg.selectAll("rect.legend")
			.data(seriesKeys)
			.join("rect")
				.classed("legend", true)
				.attr("stroke", "none")
				.attr("x", width-marginRight+7)
				.attr("y", d => (seriesKeys.length-1-seriesKeys.indexOf(d))*24+51)
				.attr("width", 10)
				.attr("height", 10)
				.attr("display", (() => {
					if (seriesKeys.length <= 3) {
						return "inherit";
					}
					return "none";
				})())
				.attr("fill", d => seriesScale(d));

		svg.selectAll("text.duration")
			.data([xDomain[1]])
			.join("text")
				.classed("duration", true)
				.attr("text-anchor", "left")
				.attr("font-size", "10px")
				.attr("x", width-marginRight+5)
				.attr("y", height-marginBottom+10)
				.attr("fill", "currentColor")
				.attr("font-weight", "bold")
				.text(d => `Total: ${d.toFixed(2)} s`);

		svg.selectAll("text.results")
			.data([[(xDomain[1]-mutTime)/xDomain[1]*100, yDomain[1]]])
			.join("text")
				.classed("results", true)
				.attr("text-anchor", "middle")
				.attr("font-size", "12px")
				.attr("x", marginLeft + (width-marginLeft-marginRight)/2)
				.attr("y", height-marginBottom+37)
				.attr("fill", "currentColor")
				.attr("font-weight", "bold")
				.text(d => `GC CPU = ${d[0].toFixed(1)}%, Peak Mem = ${d[1].toFixed(1)} MiB`);

		const peakLive = d3.max(d3.map(data, p => p["Live Heap"]));
		const otherMem = d3.max(d3.map(data, p => p["Other Mem."]));

		svg.selectAll("text.subresults")
			.data([[peakLive, otherMem]])
			.join("text")
				.classed("subresults", true)
				.attr("text-anchor", "middle")
				.attr("font-size", "11px")
				.attr("x", marginLeft + (width-marginLeft-marginRight)/2)
				.attr("y", height-marginBottom+51)
				.attr("fill", "currentColor")
				.text(d => {
					let base = "";
					if (d[0]) {
						base += `Peak Live Mem = ${d[0].toFixed(1)} MiB`;
					}
					if (d[1]) {
						base += `, Other Mem = ${d[1].toFixed(1)} MiB`;
					}
					if (base !== "") {
						base = "(" + base + ")";
					}
					return base;
				});
	}
	return [svg.node(), update];
}

function gcModel(workload, config) {
	let otherMem = config["otherMem"];
	if (typeof(otherMem) !== 'number') {
		otherMem = document.getElementById(config["otherMem"]).value;
	}
	let gogc = config["GOGC"];
	if (typeof(gogc) !== 'number') {
		gogc = document.getElementById(config["GOGC"]).value;
	}
	let memoryLimit = config["memoryLimit"];
	if (typeof(memoryLimit) !== 'number') {
		memoryLimit = document.getElementById(config["memoryLimit"]).value;
	}
	let initialLive = 0;
	if ("initialLive" in config) {
		initialLive = config["initialLive"];
	}
	let trackLive = false;
	if ("trackLive" in config) {
		trackLive = config["trackLive"];
		if (typeof(trackLive) !== 'boolean') {
			trackLive = document.getElementById(config["trackLive"]).checked;
		}
	}
	let fixedWindow = Infinity;
	if ("fixedWindow" in config) {
		fixedWindow = config["fixedWindow"];
	}
	const data = new Array();

	// State.
	const minHeapGoal = 4; // MiB
	let t = 0;
	let liveHeap = initialLive;
	let newHeap = 0;
	let liveFromCycle = new Array();
	liveFromCycle.push(initialLive);
	liveFromCycle.push(0);

	const computeHeapGoal = (liveHeap) => {
		let heapGoal = liveHeap*(1.0 + (gogc / 100));
		if (gogc === Infinity) {
			heapGoal = Infinity;
		}
		if (heapGoal+otherMem > memoryLimit) {
			heapGoal = memoryLimit - otherMem
		}
		if (gogc !== Infinity && heapGoal < minHeapGoal) {
			heapGoal = minHeapGoal
		}
		if (heapGoal < liveHeap + 0.0625) {
			heapGoal = liveHeap + 0.0625
		}
		return heapGoal
	}
	let heapGoal = computeHeapGoal(minHeapGoal / (1 + gogc/100)); // Fake a live heap for minHeapGoal.
	if (initialLive !== 0) {
		heapGoal = computeHeapGoal(initialLive);
	}

	let n = 0;
	const emit = function() {
		const datum = {"t": t};
		// The series will be automatically stacked, so for the best
		// possible presentation, we should make sure to put in
		// "other mem" first, then "live," then "new."
		// This is roughly in order of "least dynamic" series
		// to "most dynamic" which helps make the graph easier to
		// interpret.
		if (otherMem !== 0) {
			datum["Other Mem."] = otherMem;
		}
		if (trackLive) {
			for (let i = 0; i < liveFromCycle.length; i++) {
				datum[`Live Heap From GC ${i+1}`] = liveFromCycle[i];
			}
		} else {
			datum["Live Heap"] = liveHeap;
			datum["New Heap"] = newHeap;
		}
		data.push(datum)
	}

	// Emit points.
	emit();
	let nextLive = 0;
	let nextWillLive = 0;
	let nextWillDie = 0;
	let totalMutTime = 0;
	for (const work of workload) {
		let left = work.duration;
		let lastLive = liveHeap + nextLive;
		const willLive = work.duration * work.allocRate * work.newSurvivalRate;
		const willDie = lastLive * work.oldDeathRate;
		while (left > 0) {
			if (t >= fixedWindow) {
				break;
			} else if (t + left > fixedWindow) {
				left = fixedWindow - t;
			}
			let alloc = left * work.allocRate;
			let endCycle = false;
			if (liveHeap+newHeap+alloc > heapGoal) {
				alloc = heapGoal-liveHeap-newHeap;
				endCycle = true;
			}
			newHeap += alloc;

			// Calculate mutator time.
			const mutTime = alloc / work.allocRate;
			left -= mutTime;
			t += mutTime;
			totalMutTime += mutTime;
			nextLive += (willLive - willDie) * (mutTime / work.duration);

			// For tracking per-GC live memory.
			nextWillLive += willLive * (mutTime / work.duration);
			nextWillDie += willDie * (mutTime / work.duration);
			liveFromCycle[liveFromCycle.length-1] = newHeap;

			if (endCycle) {
				emit();

				liveHeap += nextLive;
				for (let i = 0; i < liveFromCycle.length; i++) {
					const live = liveFromCycle[i];
					if (live > 0) {
						if (live > nextWillDie) {
							liveFromCycle[i] -= nextWillDie;
							nextWillDie = 0;
							break;
						}
						nextWillDie -= live;
						liveFromCycle[i] = 0;
					}
				}
				liveFromCycle[liveFromCycle.length-1] = nextWillLive;

				nextLive = 0;
				nextWillLive = 0;
				nextWillDie = 0;
				newHeap = 0;
				const gcTime = liveHeap / work.scanRate + config.fixedCost;
				t += gcTime;

				emit();

				heapGoal = computeHeapGoal(liveHeap)

				liveFromCycle.push(newHeap);
			}
		}
		emit();
	}
	if (trackLive) {
		for (let i = 0; i < data.length; i++) {
			for (let j = 0; j < liveFromCycle.length; j++) {
				const key = `Live Heap From GC ${j+1}`;
				if (!(key in data[i])) {
					data[i][key] = 0;
				}
			}
		}
	}
	return [data, totalMutTime];
}

const graphs = document.querySelectorAll('.gc-guide-graph');

for (let i = 0; i < graphs.length; i++) {
	const workload = JSON.parse(graphs[i].getAttribute("data-workload"));
	const config = JSON.parse(graphs[i].getAttribute("data-config"));
	const [chart, update] = StackedAreaChart({xSeries: "t"});

	const setupSlider = function(parameter, f, fmt) {
		if (typeof(config[parameter]) !== 'number') {
			const id = config[parameter];
			const slider = document.getElementById(id);
			const display = document.getElementById(id+"-display");
			const value = f(slider.value);

			if (display) {
				display.innerHTML = fmt(value);
			}
			config[parameter] = value;

			slider.oninput = function() {
				const value = f(this.value);

				if (display) {
					display.innerHTML = fmt(value);
				}
				config[parameter] = value;

				const [data, mutTime] = gcModel(workload, config);
				update(data, mutTime);
			}
		}
	};
	const setupCheckbox = function(parameter) {
		if (parameter in config && typeof(config[parameter]) !== 'boolean') {
			const id = config[parameter];
			const checkbox = document.getElementById(id);

			config[parameter] = checkbox.checked;

			checkbox.oninput = function() {
				config[parameter] = checkbox.checked;

				const [data, mutTime] = gcModel(workload, config);
				update(data, mutTime);
			}
		}
	};
	setupSlider("otherMem", x => parseInt(x), x => `${x} MiB`);
	setupSlider("GOGC", x => {
		const v = Math.round(Math.pow(2, parseFloat(x)))
		if (v >= 1024) {
			return Infinity;
		}
		return v;
	}, x => {
		if (x === Infinity) {
			return "off";
		}
		return `${x}`;
	});
	setupSlider("memoryLimit", x => parseFloat(x), x => `${x.toFixed(1)} MiB`);
	setupCheckbox("trackLive");

	const [data, mutTime] = gcModel(workload, config);
	update(data, mutTime);
	graphs[i].appendChild(chart);
}

{'fraction_non_alphanumeric': 0.18252951404777157, 'fraction_numerical': 0.016655989750160152, 'mean_word_length': 3.2752347417840375, 'pattern_counts': {'":': 2, '<': 25, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5182361', 'n_tokens_mistral': 9398, 'n_tokens_neox': 7986, 'n_words': 1751}