Commit d9cfae12 authored by lushnikov's avatar lushnikov Committed by Commit bot

DevTools: introduce Trie data structure

This patch introduces uncompressed Trie implementation. Trie comes handy
to quickly answer questions of type "give me all words with given prefix".

Text editor autocomplete benefits immediately from the trie index.
The automapping algorithm prototype also relies heavily on the trie to
quickly query file paths.

The performance and memory analysis of the implementation:
https://goo.gl/UdgS4H

BUG=none
R=dgozman, pfeldman

Review-Url: https://codereview.chromium.org/2385093002
Cr-Commit-Position: refs/heads/master@{#422663}
parent 484d6ab4
...@@ -56,6 +56,25 @@ var UnitTest = {}; ...@@ -56,6 +56,25 @@ var UnitTest = {};
console.log(text); console.log(text);
} }
UnitTest.runTests = function(tests)
{
nextTest();
function nextTest()
{
var test = tests.shift();
if (!test) {
UnitTest.completeTest();
return;
}
UnitTest.addResult("\ntest: " + test.name);
var testPromise = test();
if (!(testPromise instanceof Promise))
testPromise = Promise.resolve();
testPromise.then(nextTest);
}
}
function completeTestOnError(message, source, lineno, colno, error) function completeTestOnError(message, source, lineno, colno, error)
{ {
UnitTest.addResult("TEST ENDED IN ERROR: " + error.stack); UnitTest.addResult("TEST ENDED IN ERROR: " + error.stack);
...@@ -93,4 +112,4 @@ var UnitTest = {}; ...@@ -93,4 +112,4 @@ var UnitTest = {};
test(); test();
} }
})(); })();
\ No newline at end of file
Verify "trie" functionality.
test: testAddWord
trie.add("hello")
trie.has("he") = false
trie.has("hello") = true
trie.has("helloo") = false
test: testAddWords
trie.add("foo")
trie.add("bar")
trie.add("bazz")
trie.has("f") = false
trie.has("ba") = false
trie.has("baz") = false
trie.has("bar") = true
trie.has("bazz") = true
test: testRemoveWord
trie.add("foo")
trie.remove("f") = false
trie.remove("fo") = false
trie.remove("fooo") = false
trie.has("foo") = true
trie.remove("foo") = true
trie.has("foo") = false
test: testAddAfterRemove
trie.add("foo")
trie.remove("foo") = true
trie.add("bar")
trie.has("foo") = false
trie.has("bar") = true
test: testWordOverwrite
trie.add("foo")
trie.add("foo")
trie.remove("foo") = true
trie.has("foo") = false
test: testRemoveNonExisting
trie.add("foo")
trie.remove("bar") = false
trie.remove("baz") = false
trie.has("foo") = true
test: testEmptyWord
trie.add("")
trie.has("") = true
trie.remove("") = true
trie.has("") = false
test: testAllWords
trie.add("foo")
trie.add("bar")
trie.add("bazzz")
trie.words() = [
foo,
bar,
bazzz
]
trie.words("f") = [
foo
]
trie.words("g") = []
trie.words("b") = [
bar,
bazzz
]
trie.words("ba") = [
bar,
bazzz
]
trie.words("bar") = [
bar
]
trie.words("barz") = []
trie.words("baz") = [
bazzz
]
test: testOneCharWords
trie.add("a")
trie.add("b")
trie.add("c")
trie.words() = [
a,
b,
c
]
test: testChainWords
trie.add("f")
trie.add("fo")
trie.add("foo")
trie.add("foo")
trie.words() = [
f,
fo,
foo
]
test: testClearTrie
trie.add("foo")
trie.add("bar")
trie.words() = [
foo,
bar
]
trie.clear()
trie.words() = []
test: testLongestPrefix
trie.add("fo")
trie.add("food")
trie.longestPrefix("fear", false) = "f"
trie.longestPrefix("fear", true) = ""
trie.longestPrefix("football", false) = "foo"
trie.longestPrefix("football", true) = "fo"
trie.longestPrefix("bar", false) = ""
trie.longestPrefix("bar", true) = ""
<html>
<head>
<base href="/inspector-debug/"></base>
<script src="/inspector-debug/Runtime.js"></script>
<script src="/inspector-unit/inspector-unit-test.js"></script>
<script>
function test() {
var trie;
UnitTest.runTests([
function testAddWord()
{
var trie = new WebInspector.Trie();
addWord(trie, "hello");
hasWord(trie, "he");
hasWord(trie, "hello");
hasWord(trie, "helloo");
},
function testAddWords()
{
var trie = new WebInspector.Trie();
addWord(trie, "foo");
addWord(trie, "bar");
addWord(trie, "bazz");
hasWord(trie, "f");
hasWord(trie, "ba");
hasWord(trie, "baz");
hasWord(trie, "bar");
hasWord(trie, "bazz");
},
function testRemoveWord()
{
var trie = new WebInspector.Trie();
addWord(trie, "foo");
removeWord(trie, "f");
removeWord(trie, "fo");
removeWord(trie, "fooo");
hasWord(trie, "foo");
removeWord(trie, "foo");
hasWord(trie, "foo");
},
function testAddAfterRemove()
{
var trie = new WebInspector.Trie();
addWord(trie, "foo");
removeWord(trie, "foo");
addWord(trie, "bar");
hasWord(trie, "foo");
hasWord(trie, "bar");
},
function testWordOverwrite()
{
var trie = new WebInspector.Trie();
addWord(trie, "foo");
addWord(trie, "foo");
removeWord(trie, "foo");
hasWord(trie, "foo");
},
function testRemoveNonExisting()
{
var trie = new WebInspector.Trie();
addWord(trie, "foo");
removeWord(trie, "bar");
removeWord(trie, "baz");
hasWord(trie, "foo");
},
function testEmptyWord()
{
var trie = new WebInspector.Trie();
addWord(trie, "");
hasWord(trie, "");
removeWord(trie, "");
hasWord(trie, "");
},
function testAllWords()
{
var trie = new WebInspector.Trie();
addWord(trie, "foo");
addWord(trie, "bar");
addWord(trie, "bazzz");
words(trie);
words(trie, "f");
words(trie, "g");
words(trie, "b");
words(trie, "ba");
words(trie, "bar");
words(trie, "barz");
words(trie, "baz");
},
function testOneCharWords()
{
var trie = new WebInspector.Trie();
addWord(trie, "a");
addWord(trie, "b");
addWord(trie, "c");
words(trie);
},
function testChainWords()
{
var trie = new WebInspector.Trie();
addWord(trie, "f");
addWord(trie, "fo");
addWord(trie, "foo");
addWord(trie, "foo");
words(trie);
},
function testClearTrie()
{
var trie = new WebInspector.Trie();
addWord(trie, "foo");
addWord(trie, "bar");
words(trie);
clear(trie);
words(trie);
},
function testLongestPrefix()
{
var trie = new WebInspector.Trie();
addWord(trie, "fo");
addWord(trie, "food");
longestPrefix(trie, "fear", false);
longestPrefix(trie, "fear", true);
longestPrefix(trie, "football", false);
longestPrefix(trie, "football", true);
longestPrefix(trie, "bar", false);
longestPrefix(trie, "bar", true);
},
]);
function hasWord(trie, word)
{
UnitTest.addResult(`trie.has("${word}") = ${trie.has(word)}`);
}
function addWord(trie, word)
{
UnitTest.addResult(`trie.add("${word}")`);
trie.add(word);
}
function removeWord(trie, word)
{
UnitTest.addResult(`trie.remove("${word}") = ${trie.remove(word)}`);
}
function words(trie, prefix)
{
var title = prefix ? `trie.words("${prefix}")` : `trie.words()`;
var words = trie.words(prefix);
var text = words.length ? `[\n ${words.join(",\n ")}\n]` : "[]";
UnitTest.addResult(title + " = " + text);
}
function clear(trie)
{
trie.clear();
UnitTest.addResult("trie.clear()");
}
function longestPrefix(trie, word, fullWordOnly)
{
UnitTest.addResult(`trie.longestPrefix("${word}", ${fullWordOnly}) = "${trie.longestPrefix(word, fullWordOnly)}"`);
}
}
</script>
</head>
<body>
Verify "trie" functionality.
</body>
</html>
...@@ -43,6 +43,7 @@ devtools_common_js_files = [ ...@@ -43,6 +43,7 @@ devtools_common_js_files = [
"front_end/common/TextRange.js", "front_end/common/TextRange.js",
"front_end/common/TextUtils.js", "front_end/common/TextUtils.js",
"front_end/common/Throttler.js", "front_end/common/Throttler.js",
"front_end/common/Trie.js",
"front_end/common/UIString.js", "front_end/common/UIString.js",
"front_end/common/WebInspector.js", "front_end/common/WebInspector.js",
"front_end/common/Worker.js", "front_end/common/Worker.js",
......
...@@ -35,6 +35,7 @@ WebInspector.TextDictionary = function() ...@@ -35,6 +35,7 @@ WebInspector.TextDictionary = function()
{ {
/** @type {!Map<string, number>} */ /** @type {!Map<string, number>} */
this._words = new Map(); this._words = new Map();
this._index = new WebInspector.Trie();
} }
WebInspector.TextDictionary.prototype = { WebInspector.TextDictionary.prototype = {
...@@ -46,6 +47,7 @@ WebInspector.TextDictionary.prototype = { ...@@ -46,6 +47,7 @@ WebInspector.TextDictionary.prototype = {
var count = this._words.get(word) || 0; var count = this._words.get(word) || 0;
++count; ++count;
this._words.set(word, count); this._words.set(word, count);
this._index.add(word);
}, },
/** /**
...@@ -56,6 +58,7 @@ WebInspector.TextDictionary.prototype = { ...@@ -56,6 +58,7 @@ WebInspector.TextDictionary.prototype = {
var count = this._words.get(word) || 0; var count = this._words.get(word) || 0;
if (!count) if (!count)
return; return;
this._index.remove(word);
if (count === 1) { if (count === 1) {
this._words.delete(word); this._words.delete(word);
return; return;
...@@ -70,12 +73,7 @@ WebInspector.TextDictionary.prototype = { ...@@ -70,12 +73,7 @@ WebInspector.TextDictionary.prototype = {
*/ */
wordsWithPrefix: function(prefix) wordsWithPrefix: function(prefix)
{ {
var words = []; return this._index.words(prefix);
for (var word of this._words.keys()) {
if (word.startsWith(prefix))
words.push(word);
}
return words;
}, },
/** /**
...@@ -99,5 +97,6 @@ WebInspector.TextDictionary.prototype = { ...@@ -99,5 +97,6 @@ WebInspector.TextDictionary.prototype = {
reset: function() reset: function()
{ {
this._words.clear(); this._words.clear();
this._index.clear();
} }
} }
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @constructor
*/
WebInspector.Trie = function()
{
this.clear();
}
WebInspector.Trie.prototype = {
/**
* @param {string} word
*/
add: function(word)
{
var node = this._root;
++this._wordsInSubtree[this._root];
for (var i = 0; i < word.length; ++i) {
var edge = word[i];
var next = this._edges[node][edge];
if (!next) {
if (this._freeNodes.length) {
// No need to reset any fields since they were properly cleaned up in remove().
next = this._freeNodes.pop();
} else {
next = this._size++;
this._isWord.push(false);
this._wordsInSubtree.push(0);
this._edges.push({ __proto__: null });
}
this._edges[node][edge] = next;
}
++this._wordsInSubtree[next];
node = next;
}
this._isWord[node] = true;
},
/**
* @param {string} word
* @return {boolean}
*/
remove: function(word)
{
if (!this.has(word))
return false;
var node = this._root;
--this._wordsInSubtree[this._root];
for (var i = 0; i < word.length; ++i) {
var edge = word[i];
var next = this._edges[node][edge];
if (!--this._wordsInSubtree[next]) {
delete this._edges[node][edge];
this._freeNodes.push(next);
}
node = next;
}
this._isWord[node] = false;
return true;
},
/**
* @param {string} word
* @return {boolean}
*/
has: function(word)
{
var node = this._root;
for (var i = 0; i < word.length; ++i) {
node = this._edges[node][word[i]];
if (!node)
return false;
}
return this._isWord[node];
},
/**
* @param {string=} prefix
* @return {!Array<string>}
*/
words: function(prefix)
{
prefix = prefix || "";
var node = this._root;
for (var i = 0; i < prefix.length; ++i) {
node = this._edges[node][prefix[i]];
if (!node)
return [];
}
var results = [];
this._dfs(node, prefix, results);
return results;
},
/**
* @param {number} node
* @param {string} prefix
* @param {!Array<string>} results
*/
_dfs: function(node, prefix, results)
{
if (this._isWord[node])
results.push(prefix);
var edges = this._edges[node];
for (var edge in edges)
this._dfs(edges[edge], prefix + edge, results);
},
/**
* @param {string} word
* @param {boolean} fullWordOnly
* @return {string}
*/
longestPrefix: function(word, fullWordOnly)
{
var node = this._root;
var wordIndex = 0;
for (var i = 0; i < word.length; ++i) {
if (!fullWordOnly || this._isWord[node])
wordIndex = i;
node = this._edges[node][word[i]];
if (!node)
break;
}
return word.substring(0, wordIndex);
},
clear: function()
{
this._size = 1;
this._root = 0;
/** @type {!Array<!Object<string, number>>} */
this._edges = [{ __proto__: null }];
/** @type {!Array<boolean>} */
this._isWord = [false];
/** @type {!Array<number>} */
this._wordsInSubtree = [0];
/** @type {!Array<number>} */
this._freeNodes = [];
}
}
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
"TextRange.js", "TextRange.js",
"TextUtils.js", "TextUtils.js",
"Throttler.js", "Throttler.js",
"Trie.js",
"UIString.js", "UIString.js",
"ModuleExtensionInterfaces.js", "ModuleExtensionInterfaces.js",
"FormatterWorkerPool.js" "FormatterWorkerPool.js"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment