<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[201047] trunk</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/201047">201047</a></dd>
<dt>Author</dt> <dd>bburg@apple.com</dd>
<dt>Date</dt> <dd>2016-05-17 14:38:17 -0700 (Tue, 17 May 2016)</dd>
</dl>
<h3>Log Message</h3>
<pre>Web Inspector: Filtering huge data grids should yield occasionally so the UI remains responsive
https://bugs.webkit.org/show_bug.cgi?id=157702
<rdar://problem/26282898>
Based on a patch by Matt Baker <mattbaker@apple.com> on 2016-05-16
Reviewed by Timothy Hatcher.
Source/WebInspectorUI:
This patch adds a new class, YieldableTask, for processing large
data sets without starving the runloop. A yieldable task takes a delegate,
an iterator that produces the items to be processed by the delegate, and
the "work interval" time slice and "idle interval" to wait between time slices.
It works by using `yield` to suspend processing when the current time
slice is exceeded, and setting a timeout to wait out the idle interval.
The iterator is responsible for deciding a good traversal order for items,
and the delegate is responsible for processing each item in turn. Tasks
cannot be reused once cancelled or processing completes.
Change DataGrid to use a yieldable task for filtering data grid nodes.
When the filtering criteria changes, cancel the task and run a new task.
* UserInterface/Base/YieldableTask.js: Added.
(WebInspector.YieldableTask.prototype.get processing):
(WebInspector.YieldableTask.prototype.get cancelled):
(WebInspector.YieldableTask.prototype.get idleInterval):
(WebInspector.YieldableTask.prototype.get workInterval):
Add getters.
(WebInspector.YieldableTask.prototype.start.createIteratorForProcessingItems):
(WebInspector.YieldableTask.prototype.start):
Set up an iterator that cranks through items to be processed until the
time slice is exceeded. Check to see if the task is cancelled before and
after calling out to the delegate to perform processing on the item.
(WebInspector.YieldableTask.prototype.cancel):
Set the cancel flag. Tell the delegate the task is finished soon.
(WebInspector.YieldableTask.prototype._processPendingItems):
Request the next item from the cranking iterator so it tries to process
more items. If it yields but still has more items to process, set a timeout
and continue processing more items after the idle interval.
(WebInspector.YieldableTask.prototype._willYield): Notify the delegate.
(WebInspector.YieldableTask.prototype._didFinish): Clear state and notify.
(WebInspector.YieldableTask):
* UserInterface/Main.html:
* UserInterface/Test.html: Add new file.
* UserInterface/Views/DataGrid.js:
(WebInspector.DataGrid):
(WebInspector.DataGrid.prototype.filterDidChange):
Cancel the currently running filter task, if any exists.
(WebInspector.DataGrid.prototype._updateFilter.createIteratorForNodesToBeFiltered):
(WebInspector.DataGrid.prototype._updateFilter):
Set up and start a new filtering task when the filter updates.
(WebInspector.DataGrid.prototype.yieldableTaskWillProcessItem):
(WebInspector.DataGrid.prototype.yieldableTaskDidYield):
(WebInspector.DataGrid.prototype.yieldableTaskDidFinish):
Batch up notifications about filtered nodes changing since this can
cause a lot of unnecessary work by event listeners.
LayoutTests:
Add tests for new Inspector utility class YieldableTask.
* inspector/unit-tests/yieldable-task-expected.txt: Added.
* inspector/unit-tests/yieldable-task.html: Added.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsChangeLog">trunk/LayoutTests/ChangeLog</a></li>
<li><a href="#trunkSourceWebInspectorUIChangeLog">trunk/Source/WebInspectorUI/ChangeLog</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceMainhtml">trunk/Source/WebInspectorUI/UserInterface/Main.html</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceTesthtml">trunk/Source/WebInspectorUI/UserInterface/Test.html</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceViewsDataGridjs">trunk/Source/WebInspectorUI/UserInterface/Views/DataGrid.js</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunkLayoutTestsinspectorunittestsyieldabletaskexpectedtxt">trunk/LayoutTests/inspector/unit-tests/yieldable-task-expected.txt</a></li>
<li><a href="#trunkLayoutTestsinspectorunittestsyieldabletaskhtml">trunk/LayoutTests/inspector/unit-tests/yieldable-task.html</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceBaseYieldableTaskjs">trunk/Source/WebInspectorUI/UserInterface/Base/YieldableTask.js</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkLayoutTestsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/LayoutTests/ChangeLog (201046 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/ChangeLog        2016-05-17 21:35:06 UTC (rev 201046)
+++ trunk/LayoutTests/ChangeLog        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -1,3 +1,17 @@
</span><ins>+2016-05-17 Brian Burg <bburg@apple.com>
+
+ Web Inspector: Filtering huge data grids should yield occasionally so the UI remains responsive
+ https://bugs.webkit.org/show_bug.cgi?id=157702
+ <rdar://problem/26282898>
+
+ Based on a patch by Matt Baker <mattbaker@apple.com> on 2016-05-16
+ Reviewed by Timothy Hatcher.
+
+ Add tests for new Inspector utility class YieldableTask.
+
+ * inspector/unit-tests/yieldable-task-expected.txt: Added.
+ * inspector/unit-tests/yieldable-task.html: Added.
+
</ins><span class="cx"> 2016-05-17 Joseph Pecoraro <pecoraro@apple.com>
</span><span class="cx">
</span><span class="cx"> console namespace breaks putting properties on console.__proto__
</span></span></pre></div>
<a id="trunkLayoutTestsinspectorunittestsyieldabletaskexpectedtxt"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/inspector/unit-tests/yieldable-task-expected.txt (0 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/inspector/unit-tests/yieldable-task-expected.txt         (rev 0)
+++ trunk/LayoutTests/inspector/unit-tests/yieldable-task-expected.txt        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -0,0 +1,70 @@
</span><ins>+Testing WebInspector.YieldableTask.
+
+
+== Running test suite: YieldableTask
+-- Running test case: ItemsAreProcessedInIterationOrder
+PASS: Item to process should be next expected item.
+PASS: Item to process should be next expected item.
+PASS: Item to process should be next expected item.
+PASS: Item to process should be next expected item.
+PASS: Item to process should be next expected item.
+The yieldable task finished.
+PASS: Task should not be cancelled.
+PASS: Task should not be processing.
+
+-- Running test case: ProcessedItemsArgumentContainsExpectedItems
+PASS: processedItems argument should contain 10 items.
+PASS: Time to process items before yielding should be greater than zero.
+PASS: processedItems argument should contain all items processed since the previous yield.
+PASS: processedItems argument should contain 10 items.
+PASS: Time to process items before yielding should be greater than zero.
+PASS: processedItems argument should contain all items processed since the previous yield.
+PASS: processedItems argument should contain 10 items.
+PASS: Time to process items before yielding should be greater than zero.
+PASS: processedItems argument should contain all items processed since the previous yield.
+PASS: processedItems argument should contain 10 items.
+PASS: Time to process items before yielding should be greater than zero.
+PASS: processedItems argument should contain all items processed since the previous yield.
+Finished processing items.
+
+-- Running test case: TaskFinishesWhenInterruptedByCancellation
+Process item: "test"
+Finished processing items.
+PASS: Task should not be processing.
+PASS: Task should be cancelled.
+
+-- Running test case: ShortTaskCompletesWithoutYielding
+PASS: Should process all items without yielding.
+Finished processing items.
+PASS: Task should not be cancelled.
+PASS: Task should not be processing.
+
+-- Running test case: ProcessItemsFromGenerator
+Process item: 1
+Process item: 2
+Process item: 4
+Process item: 8
+Process item: 16
+Process item: 32
+Process item: 64
+Process item: 128
+Finished processing items.
+PASS: Task should not be cancelled.
+PASS: Task should not be processing.
+
+-- Running test case: ProcessItemsFromMap
+Process item: [1,"one"]
+Process item: [2,"two"]
+Process item: [3,"three"]
+Finished processing items.
+PASS: Task should not be cancelled.
+PASS: Task should not be processing.
+
+-- Running test case: ProcessItemsFromSet
+Process item: 1
+Process item: 2
+Process item: 3
+Finished processing items.
+PASS: Task should not be cancelled.
+PASS: Task should not be processing.
+
</ins></span></pre></div>
<a id="trunkLayoutTestsinspectorunittestsyieldabletaskhtml"></a>
<div class="addfile"><h4>Added: trunk/LayoutTests/inspector/unit-tests/yieldable-task.html (0 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/LayoutTests/inspector/unit-tests/yieldable-task.html         (rev 0)
+++ trunk/LayoutTests/inspector/unit-tests/yieldable-task.html        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -0,0 +1,167 @@
</span><ins>+<!doctype html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+ let options = {
+ workInterval: 100,
+ };
+
+ function createTaskItems(size = 5) {
+ return Array(size).fill("test");
+ }
+
+ let suite = InspectorTest.createAsyncSuite("YieldableTask");
+
+ suite.addTestCase({
+ name: "ItemsAreProcessedInIterationOrder",
+ description: "Check that yieldableTaskWillProcessItem delegate is called on input items in order.",
+ test: (resolve, reject) => {
+ let expectedItems = createTaskItems();
+ let delegate = {
+ yieldableTaskWillProcessItem: (task, item) => {
+ let expectedItem = expectedItems.shift();
+ InspectorTest.expectThat(item === expectedItem, "Item to process should be next expected item.");
+ },
+ yieldableTaskDidFinish: (task) => {
+ InspectorTest.log("The yieldable task finished.");
+ InspectorTest.expectThat(!task.cancelled, "Task should not be cancelled.");
+ InspectorTest.expectThat(!task.processing, "Task should not be processing.");
+ resolve();
+ }
+ };
+
+ new WebInspector.YieldableTask(delegate, expectedItems.slice(), options).start();
+ }
+ });
+
+ suite.addTestCase({
+ name: "ProcessedItemsArgumentContainsExpectedItems",
+ description: "The list of processed items after a yield should contain the items that were processed by yieldableTaskWillProcessItem.",
+ test: (resolve, reject) => {
+ const itemsToProcessBeforeForcingTaskToYield = 10;
+ let actualProcessedItems = [];
+
+ let delegate = {
+ yieldableTaskWillProcessItem: (task, item) => {
+ actualProcessedItems.push(item);
+ if (actualProcessedItems.length < itemsToProcessBeforeForcingTaskToYield)
+ return;
+
+ // Busy wait until workInterval is exceeded.
+ let startTime = Date.now();
+ while (Date.now() - startTime <= task.workInterval) {}
+ },
+ yieldableTaskDidYield: (task, processedItems, elapsedTime) => {
+ InspectorTest.expectThat(itemsToProcessBeforeForcingTaskToYield === processedItems.length, `processedItems argument should contain ${itemsToProcessBeforeForcingTaskToYield} items.`);
+ InspectorTest.expectThat(elapsedTime > 0, "Time to process items before yielding should be greater than zero.");
+ InspectorTest.expectThat(Object.shallowEqual(processedItems, actualProcessedItems), "processedItems argument should contain all items processed since the previous yield.");
+ actualProcessedItems = [];
+ },
+ yieldableTaskDidFinish: (task) => {
+ InspectorTest.log("Finished processing items.");
+ resolve();
+ }
+ };
+
+ new WebInspector.YieldableTask(delegate, createTaskItems(40), options).start();
+ }
+ });
+
+ suite.addTestCase({
+ name: "TaskFinishesWhenInterruptedByCancellation",
+ description: "Check that a task can be cancelled.",
+ test: (resolve, reject) => {
+ let items = createTaskItems(40);
+ let delegate = {
+ yieldableTaskWillProcessItem: (task, item) => {
+ InspectorTest.log("Process item: " + JSON.stringify(item));
+ task.cancel();
+ },
+ yieldableTaskDidFinish: (task) => {
+ InspectorTest.log("Finished processing items.");
+ InspectorTest.expectThat(!task.processing, "Task should not be processing.");
+ InspectorTest.expectThat(task.cancelled, "Task should be cancelled.");
+ resolve();
+ }
+ };
+
+ new WebInspector.YieldableTask(delegate, items, options).start();
+ }
+ });
+
+ suite.addTestCase({
+ name: "ShortTaskCompletesWithoutYielding",
+ description: "A task with very few items should run to completion without yielding.",
+ test: (resolve, reject) => {
+ let processedItemsCount = 0;
+
+ let delegate = {
+ yieldableTaskWillProcessItem: (task, item) => {},
+ yieldableTaskDidYield: (task, processedItems, elapsedTime) => { processedItemsCount++; },
+ yieldableTaskDidFinish: (task) => {
+ // Note: we expect a fake yield notification when the final item is processed, whether
+ // or not the task actually yielded the run loop.
+ InspectorTest.expectThat(processedItemsCount === 1, "Should process all items without yielding.");
+ InspectorTest.log("Finished processing items.");
+ InspectorTest.expectThat(!task.cancelled, "Task should not be cancelled.");
+ InspectorTest.expectThat(!task.processing, "Task should not be processing.");
+ resolve();
+ }
+ };
+
+ new WebInspector.YieldableTask(delegate, createTaskItems(50)).start();
+ }
+ });
+
+ function addTestCaseForIterable(iterable, name) {
+ suite.addTestCase({
+ name: `ProcessItemsFrom${name}`,
+ description: "Check that any iterable object is supported.",
+ test: (resolve, reject) => {
+ let delegate = {
+ yieldableTaskWillProcessItem: (task, item) => {
+ InspectorTest.log("Process item: " + JSON.stringify(item));
+ },
+ yieldableTaskDidFinish: (task) => {
+ InspectorTest.log("Finished processing items.");
+ InspectorTest.expectThat(!task.cancelled, "Task should not be cancelled.");
+ InspectorTest.expectThat(!task.processing, "Task should not be processing.");
+ resolve();
+ }
+ };
+
+ new WebInspector.YieldableTask(delegate, iterable).start();
+ }
+ });
+ }
+
+ function* gen() {
+ let i = 0;
+ while (i < 8)
+ yield 1 << i++;
+ }
+ addTestCaseForIterable(gen(), "Generator");
+
+ let map = new Map;
+ map.set(1, "one");
+ map.set(2, "two");
+ map.set(3, "three");
+ addTestCaseForIterable(map, "Map");
+
+ let set = new Set;
+ set.add(1);
+ set.add(2);
+ set.add(3);
+ addTestCaseForIterable(set, "Set");
+
+ suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+ <p>Testing WebInspector.YieldableTask.</p>
+</body>
+</html>
</ins></span></pre></div>
<a id="trunkSourceWebInspectorUIChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/ChangeLog (201046 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/ChangeLog        2016-05-17 21:35:06 UTC (rev 201046)
+++ trunk/Source/WebInspectorUI/ChangeLog        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -1,5 +1,71 @@
</span><span class="cx"> 2016-05-17 Brian Burg <bburg@apple.com>
</span><span class="cx">
</span><ins>+ Web Inspector: Filtering huge data grids should yield occasionally so the UI remains responsive
+ https://bugs.webkit.org/show_bug.cgi?id=157702
+ <rdar://problem/26282898>
+
+ Based on a patch by Matt Baker <mattbaker@apple.com> on 2016-05-16
+ Reviewed by Timothy Hatcher.
+
+ This patch adds a new class, YieldableTask, for processing large
+ data sets without starving the runloop. A yieldable task takes a delegate,
+ an iterator that produces the items to be processed by the delegate, and
+ the "work interval" time slice and "idle interval" to wait between time slices.
+ It works by using `yield` to suspend processing when the current time
+ slice is exceeded, and setting a timeout to wait out the idle interval.
+
+ The iterator is responsible for deciding a good traversal order for items,
+ and the delegate is responsible for processing each item in turn. Tasks
+ cannot be reused once cancelled or processing completes.
+
+ Change DataGrid to use a yieldable task for filtering data grid nodes.
+ When the filtering criteria changes, cancel the task and run a new task.
+
+ * UserInterface/Base/YieldableTask.js: Added.
+ (WebInspector.YieldableTask.prototype.get processing):
+ (WebInspector.YieldableTask.prototype.get cancelled):
+ (WebInspector.YieldableTask.prototype.get idleInterval):
+ (WebInspector.YieldableTask.prototype.get workInterval):
+ Add getters.
+
+ (WebInspector.YieldableTask.prototype.start.createIteratorForProcessingItems):
+ (WebInspector.YieldableTask.prototype.start):
+ Set up an iterator that cranks through items to be processed until the
+ time slice is exceeded. Check to see if the task is cancelled before and
+ after calling out to the delegate to perform processing on the item.
+
+ (WebInspector.YieldableTask.prototype.cancel):
+ Set the cancel flag. Tell the delegate the task is finished soon.
+
+ (WebInspector.YieldableTask.prototype._processPendingItems):
+ Request the next item from the cranking iterator so it tries to process
+ more items. If it yields but still has more items to process, set a timeout
+ and continue processing more items after the idle interval.
+
+ (WebInspector.YieldableTask.prototype._willYield): Notify the delegate.
+ (WebInspector.YieldableTask.prototype._didFinish): Clear state and notify.
+ (WebInspector.YieldableTask):
+
+ * UserInterface/Main.html:
+ * UserInterface/Test.html: Add new file.
+
+ * UserInterface/Views/DataGrid.js:
+ (WebInspector.DataGrid):
+ (WebInspector.DataGrid.prototype.filterDidChange):
+ Cancel the currently running filter task, if any exists.
+
+ (WebInspector.DataGrid.prototype._updateFilter.createIteratorForNodesToBeFiltered):
+ (WebInspector.DataGrid.prototype._updateFilter):
+ Set up and start a new filtering task when the filter updates.
+
+ (WebInspector.DataGrid.prototype.yieldableTaskWillProcessItem):
+ (WebInspector.DataGrid.prototype.yieldableTaskDidYield):
+ (WebInspector.DataGrid.prototype.yieldableTaskDidFinish):
+ Batch up notifications about filtered nodes changing since this can
+ cause a lot of unnecessary work by event listeners.
+
+2016-05-17 Brian Burg <bburg@apple.com>
+
</ins><span class="cx"> Web Inspector: breakpoints in sourceURL named scripts are not persisted
</span><span class="cx"> https://bugs.webkit.org/show_bug.cgi?id=157714
</span><span class="cx"> <rdar://problem/26287099>
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceBaseYieldableTaskjs"></a>
<div class="addfile"><h4>Added: trunk/Source/WebInspectorUI/UserInterface/Base/YieldableTask.js (0 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Base/YieldableTask.js         (rev 0)
+++ trunk/Source/WebInspectorUI/UserInterface/Base/YieldableTask.js        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -0,0 +1,156 @@
</span><ins>+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WebInspector.YieldableTask = class YieldableTask extends WebInspector.Object
+{
+ constructor(delegate, items, options={})
+ {
+ super();
+
+ let {workInterval, idleInterval} = options;
+ console.assert(!workInterval || workInterval > 0, workInterval);
+ console.assert(!idleInterval || idleInterval > 0, idleInterval);
+
+ console.assert(delegate && typeof delegate.yieldableTaskWillProcessItem === "function", "Delegate provide an implementation of method 'yieldableTaskWillProcessItem'.");
+
+ console.assert(items instanceof Object && Symbol.iterator in items, "Argument `items` must subclass Object and be iterable.", items);
+
+ // Milliseconds to run before the task should yield.
+ this._workInterval = workInterval || 10;
+ // Milliseconds to idle before asynchronously resuming the task.
+ this._idleInterval = idleInterval || 0;
+
+ this._delegate = delegate;
+
+ this._items = items;
+ this._idleTimeoutIdentifier = undefined;
+ this._processing = false;
+ this._processing = false;
+ this._cancelled = false;
+ }
+
+ // Public
+
+ get processing() { return this._processing; }
+ get cancelled() { return this._cancelled; }
+
+ get idleInterval() { return this._idleInterval; }
+ get workInterval() { return this._workInterval; }
+
+ start()
+ {
+ console.assert(!this._processing);
+ if (this._processing)
+ return;
+
+ console.assert(!this._cancelled);
+ if (this._cancelled)
+ return;
+
+ function* createIteratorForProcessingItems()
+ {
+ let startTime = Date.now();
+ let processedItems = [];
+
+ for (let item of this._items) {
+ if (this._cancelled)
+ break;
+
+ this._delegate.yieldableTaskWillProcessItem(this, item);
+ processedItems.push(item);
+
+ // Calling out to the delegate may cause the task to be cancelled.
+ if (this._cancelled)
+ break;
+
+ let elapsedTime = Date.now() - startTime;
+ if (elapsedTime > this._workInterval) {
+ let returnedItems = processedItems.slice();
+ processedItems = [];
+ this._willYield(returnedItems, elapsedTime);
+
+ yield;
+
+ startTime = Date.now();
+ }
+ }
+
+ // The task sends a fake yield notification to the delegate so that
+ // the delegate receives notification of all processed items before finishing.
+ if (processedItems.length)
+ this._willYield(processedItems, Date.now() - startTime);
+ }
+
+ this._processing = true;
+ this._pendingItemsIterator = createIteratorForProcessingItems.call(this);
+ this._processPendingItems();
+ }
+
+ cancel()
+ {
+ if (!this._processing)
+ return;
+
+ this._cancelled = true;
+ }
+
+ // Private
+
+ _processPendingItems()
+ {
+ console.assert(this._processing);
+
+ if (this._cancelled)
+ return;
+
+ if (!this._pendingItemsIterator.next().done) {
+ this._idleTimeoutIdentifier = setTimeout(() => { this._processPendingItems(); }, this._idleInterval);
+ return;
+ }
+
+ this._didFinish();
+ }
+
+ _willYield(processedItems, elapsedTime)
+ {
+ if (typeof this._delegate.yieldableTaskDidYield === "function")
+ this._delegate.yieldableTaskDidYield(this, processedItems, elapsedTime);
+ }
+
+ _didFinish()
+ {
+ this._processing = false;
+ this._pendingItemsIterator = null;
+
+ if (this._idleTimeoutIdentifier) {
+ clearTimeout(this._idleTimeoutIdentifier);
+ this._idleTimeoutIdentifier = undefined;
+ }
+
+ if (typeof this._delegate.yieldableTaskDidFinish === "function")
+ this._delegate.yieldableTaskDidFinish(this);
+ }
+};
+
</ins></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceMainhtml"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Main.html (201046 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Main.html        2016-05-17 21:35:06 UTC (rev 201046)
+++ trunk/Source/WebInspectorUI/UserInterface/Main.html        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -238,6 +238,7 @@
</span><span class="cx"> <script src="Base/URLUtilities.js"></script>
</span><span class="cx"> <script src="Base/Utilities.js"></script>
</span><span class="cx"> <script src="Base/Setting.js"></script>
</span><ins>+ <script src="Base/YieldableTask.js"></script>
</ins><span class="cx">
</span><span class="cx"> <script src="Protocol/ProtocolTracer.js"></script>
</span><span class="cx"> <script src="Protocol/LoggingProtocolTracer.js"></script>
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceTesthtml"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Test.html (201046 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Test.html        2016-05-17 21:35:06 UTC (rev 201046)
+++ trunk/Source/WebInspectorUI/UserInterface/Test.html        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -53,6 +53,7 @@
</span><span class="cx"> <script src="Base/URLUtilities.js"></script>
</span><span class="cx"> <script src="Base/Utilities.js"></script>
</span><span class="cx"> <script src="Base/Setting.js"></script>
</span><ins>+ <script src="Base/YieldableTask.js"></script>
</ins><span class="cx">
</span><span class="cx"> <script src="Protocol/ProtocolTracer.js"></script>
</span><span class="cx"> <script src="Protocol/LoggingProtocolTracer.js"></script>
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceViewsDataGridjs"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Views/DataGrid.js (201046 => 201047)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Views/DataGrid.js        2016-05-17 21:35:06 UTC (rev 201046)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/DataGrid.js        2016-05-17 21:38:17 UTC (rev 201047)
</span><span class="lines">@@ -62,6 +62,7 @@
</span><span class="cx">
</span><span class="cx"> this._filterText = "";
</span><span class="cx"> this._filterDelegate = null;
</span><ins>+ this._filterDidModifyNodeWhileProcessingItems = false;
</ins><span class="cx">
</span><span class="cx"> this.element.className = "data-grid";
</span><span class="cx"> this.element.tabIndex = 0;
</span><span class="lines">@@ -329,6 +330,11 @@
</span><span class="cx"> if (this._scheduledFilterUpdateIdentifier)
</span><span class="cx"> return;
</span><span class="cx">
</span><ins>+ if (this._applyFilterToNodesTask) {
+ this._applyFilterToNodesTask.cancel();
+ this._applyFilterToNodesTask = null;
+ }
+
</ins><span class="cx"> this._scheduledFilterUpdateIdentifier = requestAnimationFrame(this._updateFilter.bind(this));
</span><span class="cx"> }
</span><span class="cx">
</span><span class="lines">@@ -1754,28 +1760,57 @@
</span><span class="cx">
</span><span class="cx"> this._textFilterRegex = simpleGlobStringToRegExp(this._filterText, "i");
</span><span class="cx">
</span><del>- // Don't populate if we don't have any active filters.
- // We only need to populate when a filter needs to reveal.
- let dontPopulate = !this._textFilterRegex && !this.hasCustomFilters();
</del><ins>+ if (this._applyFilterToNodesTask && this._applyFilterToNodesTask.processing)
+ this._applyFilterToNodesTask.cancel();
</ins><span class="cx">
</span><del>- let filterDidModifyNode = false;
- let currentNode = this._rows[0];
- while (currentNode && !currentNode.root) {
- const currentNodeWasHidden = currentNode.hidden;
- this._applyFiltersToNode(currentNode);
- if (currentNodeWasHidden !== currentNode.hidden) {
- this.dispatchEventToListeners(WebInspector.DataGrid.Event.NodeWasFiltered, {node: currentNode});
- filterDidModifyNode = true;
</del><ins>+ function *createIteratorForNodesToBeFiltered()
+ {
+ // Don't populate if we don't have any active filters.
+ // We only need to populate when a filter needs to reveal.
+ let dontPopulate = !this._textFilterRegex && !this.hasCustomFilters();
+
+ let currentNode = this._rows[0];
+ while (currentNode && !currentNode.root) {
+ yield currentNode;
+ currentNode = currentNode.traverseNextNode(false, null, dontPopulate);
</ins><span class="cx"> }
</span><del>-
- currentNode = currentNode.traverseNextNode(false, null, dontPopulate);
</del><span class="cx"> }
</span><span class="cx">
</span><del>- if (!filterDidModifyNode)
</del><ins>+ let items = createIteratorForNodesToBeFiltered.call(this);
+ this._applyFilterToNodesTask = new WebInspector.YieldableTask(this, items, {workInterval: 100});
+
+ this._filterDidModifyNodeWhileProcessingItems = false;
+
+ this._applyFilterToNodesTask.start();
+ }
+
+ // YieldableTask delegate
+
+ yieldableTaskWillProcessItem(task, node)
+ {
+ const nodeWasHidden = node.hidden;
+ this._applyFiltersToNode(node);
+ if (nodeWasHidden === node.hidden)
</ins><span class="cx"> return;
</span><span class="cx">
</span><ins>+ this.dispatchEventToListeners(WebInspector.DataGrid.Event.NodeWasFiltered, {node});
+ this._filterDidModifyNodeWhileProcessingItems = true;
+ }
+
+ yieldableTaskDidYield(task, processedItems, elapsedTime)
+ {
+ if (!this._filterDidModifyNodeWhileProcessingItems)
+ return;
+
+ this._filterDidModifyNodeWhileProcessingItems = false;
+
</ins><span class="cx"> this.dispatchEventToListeners(WebInspector.DataGrid.Event.FilterDidChange);
</span><span class="cx"> }
</span><ins>+
+ yieldableTaskDidFinish(task)
+ {
+ this._applyFilterToNodesTask = null;
+ }
</ins><span class="cx"> };
</span><span class="cx">
</span><span class="cx"> WebInspector.DataGrid.Event = {
</span></span></pre>
</div>
</div>
</body>
</html>