mirror of https://github.com/webpack/webpack.git
				
				
				
			
		
			
				
	
	
		
			252 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			252 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| "use strict";
 | |
| 
 | |
| // Simulations show these probabilities for a single change
 | |
| // 93.1% that one group is invalidated
 | |
| // 4.8% that two groups are invalidated
 | |
| // 1.1% that 3 groups are invalidated
 | |
| // 0.1% that 4 or more groups are invalidated
 | |
| //
 | |
| // And these for removing/adding 10 lexically adjacent files
 | |
| // 64.5% that one group is invalidated
 | |
| // 24.8% that two groups are invalidated
 | |
| // 7.8% that 3 groups are invalidated
 | |
| // 2.7% that 4 or more groups are invalidated
 | |
| //
 | |
| // And these for removing/adding 3 random files
 | |
| // 0% that one group is invalidated
 | |
| // 3.7% that two groups are invalidated
 | |
| // 80.8% that 3 groups are invalidated
 | |
| // 12.3% that 4 groups are invalidated
 | |
| // 3.2% that 5 or more groups are invalidated
 | |
| 
 | |
| /**
 | |
|  *
 | |
|  * @param {string} a key
 | |
|  * @param {string} b key
 | |
|  * @returns {number} the similarity as number
 | |
|  */
 | |
| const similarity = (a, b) => {
 | |
| 	const l = Math.min(a.length, b.length);
 | |
| 	let dist = 0;
 | |
| 	for (let i = 0; i < l; i++) {
 | |
| 		const ca = a.charCodeAt(i);
 | |
| 		const cb = b.charCodeAt(i);
 | |
| 		dist += Math.max(0, 10 - Math.abs(ca - cb));
 | |
| 	}
 | |
| 	return dist;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @param {string} a key
 | |
|  * @param {string} b key
 | |
|  * @returns {string} the common part and a single char for the difference
 | |
|  */
 | |
| const getName = (a, b) => {
 | |
| 	const l = Math.min(a.length, b.length);
 | |
| 	let r = "";
 | |
| 	for (let i = 0; i < l; i++) {
 | |
| 		const ca = a.charAt(i);
 | |
| 		const cb = b.charAt(i);
 | |
| 		r += ca;
 | |
| 		if (ca === cb) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		return r;
 | |
| 	}
 | |
| 	return a;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * @template T
 | |
|  */
 | |
| class Node {
 | |
| 	/**
 | |
| 	 * @param {T} item item
 | |
| 	 * @param {string} key key
 | |
| 	 * @param {number} size size
 | |
| 	 */
 | |
| 	constructor(item, key, size) {
 | |
| 		this.item = item;
 | |
| 		this.key = key;
 | |
| 		this.size = size;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @template T
 | |
|  */
 | |
| class Group {
 | |
| 	/**
 | |
| 	 * @param {Node<T>[]} nodes nodes
 | |
| 	 * @param {number[]} similarities similarities between the nodes (length = nodes.length - 1)
 | |
| 	 */
 | |
| 	constructor(nodes, similarities) {
 | |
| 		this.nodes = nodes;
 | |
| 		this.similarities = similarities;
 | |
| 		this.size = nodes.reduce((size, node) => size + node.size, 0);
 | |
| 		/** @type {string} */
 | |
| 		this.key = undefined;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @template T
 | |
|  * @typedef {Object} GroupedItems<T>
 | |
|  * @property {string} key
 | |
|  * @property {T[]} items
 | |
|  * @property {number} size
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @template T
 | |
|  * @typedef {Object} Options
 | |
|  * @property {number} maxSize maximum size of a group
 | |
|  * @property {number} minSize minimum size of a group (preferred over maximum size)
 | |
|  * @property {Iterable<T>} items a list of items
 | |
|  * @property {function(T): number} getSize function to get size of an item
 | |
|  * @property {function(T): string} getKey function to get the key of an item
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @template T
 | |
|  * @param {Options<T>} options options object
 | |
|  * @returns {GroupedItems<T>[]} grouped items
 | |
|  */
 | |
| module.exports = ({ maxSize, minSize, items, getSize, getKey }) => {
 | |
| 	/** @type {Group<T>[]} */
 | |
| 	const result = [];
 | |
| 
 | |
| 	const nodes = Array.from(
 | |
| 		items,
 | |
| 		item => new Node(item, getKey(item), getSize(item))
 | |
| 	);
 | |
| 
 | |
| 	/** @type {Node<T>[]} */
 | |
| 	const initialNodes = [];
 | |
| 
 | |
| 	// return nodes bigger than maxSize directly as group
 | |
| 	for (const node of nodes) {
 | |
| 		if (node.size >= maxSize) {
 | |
| 			result.push(new Group([node], []));
 | |
| 		} else {
 | |
| 			initialNodes.push(node);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (initialNodes.length > 0) {
 | |
| 		// lexically ordering of keys
 | |
| 		initialNodes.sort((a, b) => {
 | |
| 			if (a.key < b.key) return -1;
 | |
| 			if (a.key > b.key) return 1;
 | |
| 			return 0;
 | |
| 		});
 | |
| 
 | |
| 		// calculate similarities between lexically adjacent nodes
 | |
| 		/** @type {number[]} */
 | |
| 		const similarities = [];
 | |
| 		for (let i = 1; i < initialNodes.length; i++) {
 | |
| 			const a = initialNodes[i - 1];
 | |
| 			const b = initialNodes[i];
 | |
| 			similarities.push(similarity(a.key, b.key));
 | |
| 		}
 | |
| 
 | |
| 		const queue = [new Group(initialNodes, similarities)];
 | |
| 
 | |
| 		while (queue.length) {
 | |
| 			const group = queue.pop();
 | |
| 			// only groups bigger than maxSize need to be splitted
 | |
| 			if (group.size < maxSize) {
 | |
| 				result.push(group);
 | |
| 				continue;
 | |
| 			}
 | |
| 
 | |
| 			// find unsplittable area from left and right
 | |
| 			// going minSize from left and right
 | |
| 			let left = 0;
 | |
| 			let leftSize = 0;
 | |
| 			while (leftSize < minSize) {
 | |
| 				leftSize += group.nodes[left].size;
 | |
| 				left++;
 | |
| 			}
 | |
| 			let right = group.nodes.length - 1;
 | |
| 			let rightSize = 0;
 | |
| 			while (rightSize < minSize) {
 | |
| 				rightSize += group.nodes[right].size;
 | |
| 				right--;
 | |
| 			}
 | |
| 
 | |
| 			if (left - 1 > right) {
 | |
| 				// can't split group while holding minSize
 | |
| 				// because minSize is preferred of maxSize we return
 | |
| 				// the group here even while it's too big
 | |
| 				// To avoid this make sure maxSize > minSize * 3
 | |
| 				result.push(group);
 | |
| 				continue;
 | |
| 			}
 | |
| 			if (left <= right) {
 | |
| 				// when there is a area between left and right
 | |
| 				// we look for best split point
 | |
| 				// we split at the minimum similiarity
 | |
| 				// here key space is separated the most
 | |
| 				let best = left - 1;
 | |
| 				let bestSimilarity = group.similarities[best];
 | |
| 				for (let i = left; i <= right; i++) {
 | |
| 					const similarity = group.similarities[i];
 | |
| 					if (similarity < bestSimilarity) {
 | |
| 						best = i;
 | |
| 						bestSimilarity = similarity;
 | |
| 					}
 | |
| 				}
 | |
| 				left = best + 1;
 | |
| 				right = best;
 | |
| 			}
 | |
| 
 | |
| 			// create two new groups for left and right area
 | |
| 			// and queue them up
 | |
| 			const rightNodes = [group.nodes[right + 1]];
 | |
| 			/** @type {number[]} */
 | |
| 			const rightSimilaries = [];
 | |
| 			for (let i = right + 2; i < group.nodes.length; i++) {
 | |
| 				rightSimilaries.push(group.similarities[i - 1]);
 | |
| 				rightNodes.push(group.nodes[i]);
 | |
| 			}
 | |
| 			queue.push(new Group(rightNodes, rightSimilaries));
 | |
| 
 | |
| 			const leftNodes = [group.nodes[0]];
 | |
| 			/** @type {number[]} */
 | |
| 			const leftSimilaries = [];
 | |
| 			for (let i = 1; i < left; i++) {
 | |
| 				leftSimilaries.push(group.similarities[i - 1]);
 | |
| 				leftNodes.push(group.nodes[i]);
 | |
| 			}
 | |
| 			queue.push(new Group(leftNodes, leftSimilaries));
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// lexically ordering
 | |
| 	result.sort((a, b) => {
 | |
| 		if (a.nodes[0].key < b.nodes[0].key) return -1;
 | |
| 		if (a.nodes[0].key > b.nodes[0].key) return 1;
 | |
| 		return 0;
 | |
| 	});
 | |
| 
 | |
| 	// give every group a name
 | |
| 	for (let i = 0; i < result.length; i++) {
 | |
| 		const group = result[i];
 | |
| 		const first = group.nodes[0];
 | |
| 		const last = group.nodes[group.nodes.length - 1];
 | |
| 		let name = getName(first.key, last.key);
 | |
| 		group.key = name;
 | |
| 	}
 | |
| 
 | |
| 	// return the results
 | |
| 	return result.map(group => {
 | |
| 		/** @type {GroupedItems} */
 | |
| 		return {
 | |
| 			key: group.key,
 | |
| 			items: group.nodes.map(node => node.item),
 | |
| 			size: group.size
 | |
| 		};
 | |
| 	});
 | |
| };
 |