| // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:collection' show SplayTreeMap; | |
| /// Assigns names from [nameSequence] to items using a naive algorithm. | |
| /// | |
| /// Items are assigned the next available name in decreasing frequency | |
| /// order. This allocation order is unstable in that small changes in the input | |
| /// cause large changes in the output. To see this, consider a typical | |
| /// distribution that has a 'tail' where large numbers of items occur two or | |
| /// three times. If the small change causes one of the items that occurs twice | |
| /// to occur four times, of then all the items that occur three times will be | |
| /// assigned names that are 'shifted down' in the allocation order, as will many | |
| /// of the items that occur twice. | |
| /// | |
| /// See [semistableFrequencyAssignment] for meaning of parameters. | |
| void naiveFrequencyAssignment( | |
| int items, | |
| Iterable<String> nameSequence, | |
| int Function(int) hashOf, | |
| int Function(int) countOf, | |
| void Function(int, String) assign, | |
| ) { | |
| Iterator<String> nameIterator = nameSequence.iterator..moveNext(); | |
| for (int item = 0; item < items; item++) { | |
| assign(item, nameIterator.current); | |
| nameIterator.moveNext(); | |
| } | |
| } | |
| /// Assigns names from [nameSequence] to items, trying to avoid the unstable | |
| /// allocation of [naiveFrequencyAssignment] by assigning items to their | |
| /// hashing-based 'preferred' slots if possible. | |
| /// | |
| /// - [items]: number of items to be assigned names. Items are numbered from | |
| /// zero. Items must be sorted in order of decreasing [countOf]. | |
| /// | |
| /// - [nameSequence]: Potentially unbounded sequence of valid names in | |
| /// increasing size. | |
| /// | |
| /// - [hashOf]: Function returning a stable hash code for item `i`. | |
| /// | |
| /// - [countOf]: Function returning the frequency or number of occurrences of | |
| /// item `i`. | |
| /// | |
| /// - [assign]: Function to register the assignment of a name to item `i`. | |
| void semistableFrequencyAssignment( | |
| int items, | |
| Iterable<String> nameSequence, | |
| int Function(int) hashOf, | |
| int Function(int) countOf, | |
| void Function(int, String) assign, | |
| ) { | |
| // Overallocate 3x the number of names so that the last pool is large enough | |
| // to substantially reduce collisions. Round up to the next power of two so | |
| // that slightly changing the number of items does not usually change the size | |
| // of the largest pool. | |
| int maxIndex = 1 << (items * 3).bitLength; | |
| List<String> names = nameSequence.take(maxIndex).toList(growable: false); | |
| List<_Pool> pools = _Pool.makePools(names); | |
| // First cohort with unassigned items. | |
| _Cohort? firstCohort = _Cohort.makeCohorts(items, countOf); | |
| for (var pool in pools) { | |
| // Completely allocate smaller pools before allocating larger | |
| // pools. Completely allocate each cohort in turn. | |
| // TODO(sra): If the next several cohorts all fit in the current pool, the | |
| // allocation will not change the bytes saved by minification. Consider | |
| // allocating the preferred slot from several cohorts before allocating a | |
| // non-preferred slot. This should increase the number of items allocated to | |
| // their preferred slot. | |
| firstCohort = firstCohort?.skipEmpty(); | |
| for ( | |
| var startCohort = firstCohort; | |
| startCohort != null; | |
| startCohort = startCohort.next | |
| ) { | |
| if (pool.remaining == 0) break; | |
| // Pass 1: assign members of cohort their preferred slot if available. | |
| List<int> assigned = []; | |
| for (var item in startCohort.unassigned) { | |
| if (pool.remaining == 0) break; | |
| int hash = hashOf(item); | |
| int slot = hash % pool.size; | |
| if (pool.slotIsAvailable(slot)) { | |
| assign(item, pool.allocate(slot)); | |
| assigned.add(item); | |
| } | |
| } | |
| startCohort.unassigned.removeAll(assigned); | |
| // Pass 2: assign members of cohort their second 'rehash' slot if | |
| // available, or the next available slot. | |
| assigned.clear(); | |
| for (var item in startCohort.unassigned) { | |
| if (pool.remaining == 0) break; | |
| int hash = hashOf(item); | |
| int rehashSlot = (5 * hash + 7) % pool.size; | |
| int slot = pool.firstAvailableSlotFrom(rehashSlot); | |
| assign(item, pool.allocate(slot)); | |
| assigned.add(item); | |
| } | |
| startCohort.unassigned.removeAll(assigned); | |
| } | |
| } | |
| // Perform naive assignment of any items left unassigned above (should be | |
| // none). | |
| for (var pool in pools) { | |
| while (pool.remaining > 0) { | |
| firstCohort = firstCohort?.skipEmpty(); | |
| if (firstCohort == null) break; | |
| var item = firstCohort.unassigned.first; | |
| String name = pool.allocate(pool.firstAvailableSlot()); | |
| assign(item, name); | |
| firstCohort.unassigned.remove(item); | |
| } | |
| } | |
| } | |
| /// A [_Pool] is a set of identifiers of the same length from which names are | |
| /// allocated. | |
| class _Pool { | |
| final List<String?> _names = []; | |
| // Keep the unused (available) slots in an ordered set for efficiently finding | |
| // the next available slot (i.e. linear rehash). We are concerned about | |
| // efficiency because the smaller pools are completely allocated, making | |
| // worst-case linear rehashing quadratic. Using an ordered map brings this | |
| // down to O(N log N). | |
| // | |
| // We would prefer an ordered Set, but SplayTreeSet does not have methods to | |
| // find keys adjacent to a query key. | |
| final SplayTreeMap<int, bool> _availableSlots = SplayTreeMap(); | |
| static List<_Pool> makePools(Iterable<String> names) { | |
| List<_Pool> pools = []; | |
| for (var name in names) { | |
| int length = name.length; | |
| while (pools.length < length) { | |
| pools.add(_Pool()); | |
| } | |
| _Pool pool = pools[length - 1]; | |
| pool._availableSlots[pool._names.length] = true; | |
| pool._names.add(name); | |
| } | |
| return pools; | |
| } | |
| int get size => _names.length; | |
| int get remaining => _availableSlots.length; | |
| bool slotIsAvailable(int slot) => _names[slot] != null; | |
| int firstAvailableSlot() => _availableSlots.keys.first; | |
| /// Returns [start] if slot [start] is free, otherwise returns the next | |
| /// available slot. | |
| int firstAvailableSlotFrom(int start) => | |
| _availableSlots.firstKeyAfter(start - 1) ?? | |
| _availableSlots.firstKeyAfter(-1) ?? | |
| (throw StateError('No entries left in pool')); | |
| String allocate(int slot) { | |
| String name = _names[slot]!; | |
| _names[slot] = null; | |
| _availableSlots.remove(slot); | |
| return name; | |
| } | |
| @override | |
| String toString() => '_Pool(${_names.length}, ${_availableSlots.length})'; | |
| } | |
| /// A [_Cohort] is a set of entities which occur with the same frequency. The | |
| /// entities are identified by integers. | |
| class _Cohort { | |
| _Cohort? next; // Next cohort in decreasing frequency. | |
| final int count; // This is the cohort of items occurring [count] times. | |
| Set<int> unassigned = {}; | |
| _Cohort(this.count); | |
| _Cohort? skipEmpty() { | |
| _Cohort? cohort = this; | |
| while (cohort != null && cohort.remaining == 0) { | |
| cohort = cohort.next; | |
| } | |
| return cohort; | |
| } | |
| int get remaining => unassigned.length; | |
| static _Cohort? makeCohorts(int items, int Function(int) countOf) { | |
| // Build _Cohorts. | |
| _Cohort? first, current; | |
| int lastCount = -1; | |
| for (int item = 0; item < items; item++) { | |
| int count = countOf(item); | |
| if (count != lastCount) { | |
| lastCount = count; | |
| _Cohort next = _Cohort(count); | |
| if (current == null) { | |
| first = next; | |
| } else { | |
| current.next = next; | |
| } | |
| current = next; | |
| } | |
| current!.unassigned.add(item); | |
| } | |
| return first; | |
| } | |
| @override | |
| String toString() => '_Cohort($count, $remaining)'; | |
| } |