Skip to content

Support generic chunking and chunk Bind's DOM scan#6967

Merged
dreamofabear merged 15 commits intoampproject:masterfrom
dreamofabear:chunk-scan
Jan 27, 2017
Merged

Support generic chunking and chunk Bind's DOM scan#6967
dreamofabear merged 15 commits intoampproject:masterfrom
dreamofabear:chunk-scan

Conversation

@dreamofabear
Copy link
Copy Markdown

Partial for #6199 and #6910.

  • Add new PriorityQueue data structure
  • Extend chunk.js to support non-startup tasks
  • Chunk Bind's DOM scan

This PR is on the larger side -- happy to split this into multiple PRs. PTAL!

/to @cramforce @dvoytenko

export default class PriorityQueue {
constructor() {
/** @private @const {Array<{item: T, priority: number}>} */
this.queue_ = [];
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd get theoretically better performance with a max heap for large N but I thought it was unnecessary for current usage. I'm not sure that even the binary search is worth the readability cost.

/** @type {!Array<./bind-evaluator.EvaluateeDef>} */
const evaluatees = [];

// TODO(choumx): Use TreeWalker if this is too slow.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure from past experience that TreeWalker and getElementsByTagName will be generally comparable, but TreeWalker would be somewhat faster.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually meant to use TreeWalker so the traversal can be amortized over several frames. In my profiling, this call only took a few ms so I'll work on expression parsing first (which is much slower).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently TreeWalker is really fast.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks for the link.


// Helper function for scanning a slice of elements.
const scanFromTo = (start, end) => {
for (let i = start; i < Math.min(end, elements.length); i++) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't remember 100%, but I believe calling elements.length is here would force some significant amount of work. It might be better to iterate until we arrive at elements[++i] === undefined.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good tip. Looks like length is slow for live collections. Done.

* @return {string}
*/
get name() {
return this.fn_.displayName || this.fn_.name;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we decided not to use getters like this since they are somewhat slow. /cc @jridgewell .

Also, displayName is good for local debug, but most are likely stripped by compiler for prod binaries.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably slower than property access, but what about vs. normal function invocation?

This usage of displayName was ported from original code on line 175.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getters and setters are considerably slower than method invocation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reference. Changed to plain method.

@dvoytenko
Copy link
Copy Markdown
Contributor

Thanks, @choumx!

I'd like @cramforce to review in more detail the chunking code changes.

I left few comments, but some of a high-level picture questions I have:

  1. Do we really need priority queue? I understand all startup chunking should be done before amp-binder is instrumented, right?
  2. Is chunking the right instrument here? Or should we look into what Resources manager is doing?
  3. Is there some condition (e.g. user action) where we need to force-execute all pending crawl operations before we proceed with evaluations? Is this something that can be accomplished with chunking/idle callbacks?

};

// Divide elements into buckets to be scanned, one bucket per chunk.
const bucketSize = 10;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not the right way to do this when you design for requestIdleCallback, but this is a perfect use case for RIC!

The current chunking code has the issue that all tasks that we scheduled could not be split up further (easily). Here you can basically do work until you shouldn't anymore. With that the timeRemaining function should be made available to the scanner and it should always try to do as much as it can given the time available.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Added support for this by passing the RIC info object into the chunk function. This way, we can still prioritize usage of requestIdleCallback rather than "first-always-wins".

});

// Helper function for scanning a slice of elements.
const scanFromTo = (start, end) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long does a full run take on a typical mobile device?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a page with 5000 elements with 1000 bindings:

  • DOM scan: 100ms
  • Expression parsing: 1s

Complex expressions still take ~1ms each to parse during init. Next step is to either chunk expression parsing or convert BindEvaluator into a web worker.

Copy link
Copy Markdown
Author

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

Do we really need priority queue? I understand all startup chunking should be done before amp-binder is instrumented, right?

True, though generic background task execution is probably useful to have. Using requestIdleCallback directly has the downside that it's not widely supported and callbacks are executed in order of invocation (which may not be desired).

Is chunking the right instrument here? Or should we look into what Resources manager is doing?

Would you mind elaborating on what you mean by Resources?

Is there some condition (e.g. user action) where we need to force-execute all pending crawl operations before we proceed with evaluations? Is this something that can be accomplished with chunking/idle callbacks?

Good question. It's feasible to implement timeouts with chunk(), but not blocking the UI is the correct UX trade off IMO. Nonetheless it's still important that for vast majority of pages, this completes soon after page render.


// Helper function for scanning a slice of elements.
const scanFromTo = (start, end) => {
for (let i = start; i < Math.min(end, elements.length); i++) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good tip. Looks like length is slow for live collections. Done.

/** @type {!Array<./bind-evaluator.EvaluateeDef>} */
const evaluatees = [];

// TODO(choumx): Use TreeWalker if this is too slow.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually meant to use TreeWalker so the traversal can be amortized over several frames. In my profiling, this call only took a few ms so I'll work on expression parsing first (which is much slower).

* @return {string}
*/
get name() {
return this.fn_.displayName || this.fn_.name;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably slower than property access, but what about vs. normal function invocation?

This usage of displayName was ported from original code on line 175.

});

// Helper function for scanning a slice of elements.
const scanFromTo = (start, end) => {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a page with 5000 elements with 1000 bindings:

  • DOM scan: 100ms
  • Expression parsing: 1s

Complex expressions still take ~1ms each to parse during init. Next step is to either chunk expression parsing or convert BindEvaluator into a web worker.

};

// Divide elements into buckets to be scanned, one bucket per chunk.
const bucketSize = 10;
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Added support for this by passing the RIC info object into the chunk function. This way, we can still prioritize usage of requestIdleCallback rather than "first-always-wins".

@dvoytenko
Copy link
Copy Markdown
Contributor

Would you mind elaborating on what you mean by Resources?

I meant Resources class. It has some knowledge of host "busy" the system is: whether the document is currently being actively scrolled, which elements are being layout and with what priority.

Good question. It's feasible to implement timeouts with chunk(), but not blocking the UI is the correct UX trade off IMO. Nonetheless it's still important that for vast majority of pages, this completes soon after page render.

I think we need to find a paradigm that will work here w/o risk conditions. I see your point and agree that we don't want to block user action. So, instead, I think we need to have a model where we update data structures in memory and mark them as "user action". When a dependent bind expression is ingested, we determine whether it's in initial state (no work) or update by user action (update as soon as practical).

* @param {function(?IdleDeadline)} fn
*/
export function chunk(nodeOrAmpDoc, fn) {
export function startupChunk(nodeOrAmpDoc, fn) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why startupChunk implies "deactivate-able" while normal chunk is always "active".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chunking used to only be used for startup tasks, so I guess the nochunking=1 test query was very useful for testing. There's less of a need for generic background task chunks.

Added it below though since it's better to be consistent here.

src/chunk.js Outdated
this.viewer_ = null;

if (viewerOrPromise instanceof Promise) {
viewerOrPromise.then(viewer => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the sync case absolutely necessary?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. schedule_() is called synchronously after a task is queued/executed, which calls StartupTask#immediateTriggerCondition_(), whose return value depends on the existence of viewer_.

We can change schedule_() to be executed as a micro-task instead, but I'd rather avoid behavioral changes in this already large-ish PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please. Everything here is very subtle.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Used existing resolved Promise and added new executeASAP_() to be stubbed in tests instead.

t();
t.runTask(idleDeadline);
} catch (e) {
// We run early in init. All errors should show the doc.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch block can be removed now.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Done.

@dreamofabear
Copy link
Copy Markdown
Author

dreamofabear commented Jan 18, 2017

@dvoytenko

I meant Resources class. It has some knowledge of host "busy" the system is: whether the document is currently being actively scrolled, which elements are being layout and with what priority.

Ah I see, thanks. IMO, requestIdleCallback is the right tool here since it gives us exactly what we want: executing tasks during page idle time. Wrapping RIC with chunk is useful because:

  • Allows backup functionality when RIC is not supported (Safari and Edge/IE)
  • Allows prioritization of tasks passed to RIC rather than strict FIFO

Somewhat related to Resources: Malte mentioned it's possible to polyfill requestIdleCallback with RAF, but it's best-effort and may cause battery drain. I think that may be overkill for Bind's use case.

So, instead, I think we need to have a model where we update data structures in memory and mark them as "user action". When a dependent bind expression is ingested, we determine whether it's in initial state (no work) or update by user action (update as soon as practical).

So actually this is only used during Bind's initialization (DOM scan) during page load. This won't affect latency when updating Bind state from user actions.

@dvoytenko
Copy link
Copy Markdown
Contributor

@choumx

So, instead, I think we need to have a model where we update data structures in memory and mark them as "user action". When a dependent bind expression is ingested, we determine whether it's in initial state (no work) or update by user action (update as soon as practical).

So actually this is only used during Bind's initialization (DOM scan) during page load. This won't affect latency when updating Bind state from user actions

Let me clarify this. Imagine the following sequence:

  1. We start expression discovery/parsing. It has a number of bindings and one of the last is bound to selectedIndex var, e.g. [disabled]="selectedIndex == 0". Let's call it button1. However, our discovery/parsing haven't made it that far.
  2. Use scrolls the carousel to the next slide. on-action updates the selectedIndex (on="tap:AMP.setState(selectedIndex=event.slide)").
  3. Binder service executes data change over the so-far-known bindings. However, button1 is not parsed yet, so it's not updated here.
  4. Finally, discovery/parser parses button1. But since it's the "initial" pass, the expression is not evaluated and the button remains disabled since the user action is not recognized.

If my reading of this is correct, we could change this in a couple of ways:

  • Change step (3) to lock on parsing all expressions. In this we need to have a mechanism to force-complete discovery/parsing.
  • Or we could change step (4) and stipulate that after the user action, we execute and update expressions since they may have been updated by the user action. Or we could be more precise and mark each variable (selectedIndex in this case) as updated by user action and automatically apply existing expressions.

Does this make sense?

@dreamofabear
Copy link
Copy Markdown
Author

dreamofabear commented Jan 18, 2017

@dvoytenko

Or we could change step (4) and stipulate that after the user action, we execute and update expressions since they may have been updated by the user action.

This actually exists in this PR via flag digestQueuedAfterScan_ in bind-impl.js. This reevaluates all expressions rather than just those affected by the user action, but should be fast with the new AST optimization.

// Trigger verify-only digest in development.
const development = getMode().development;
if (development || this.digestQueuedAfterScan_) {
this.digest_(/* opt_verifyOnly */ development);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, should not this then be !this.digestQueuedAfterScan_?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Thanks.

if (!opt_skipDigest) {
this.digest_();
// If scan hasn't completed yet, set `digestQueuedAfterScan_`.
if (this.boundElements_) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't boundElements_ always truthy?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, bad merge I think. Thanks!

@dvoytenko
Copy link
Copy Markdown
Contributor

@choumx

This actually exists in this PR via flag digestQueuedAfterScan_ in bind-impl.js. This reevaluates all expressions rather than just those affected by the user action, but should be fast with the new AST optimization.

I see. This is what I wanted to see. So, in our world, AMP.setState() is always equivalent to the user action, right?

Copy link
Copy Markdown
Author

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dvoytenko

So, in our world, AMP.setState() is always equivalent to the user action, right?

Yes, with the exception of initializing <amp-state> components.

// Trigger verify-only digest in development.
const development = getMode().development;
if (development || this.digestQueuedAfterScan_) {
this.digest_(/* opt_verifyOnly */ development);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Thanks.

if (!opt_skipDigest) {
this.digest_();
// If scan hasn't completed yet, set `digestQueuedAfterScan_`.
if (this.boundElements_) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, bad merge I think. Thanks!


// Helper function for scanning a slice of elements.
const scanFromTo = (start, end) => {
for (let i = start; i < end && elements[i]; i++) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimization: Hoist assignment of elements[i] into loop header to avoid accessing the live-list twice per entry.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (idleDeadline) {
if (idleDeadline.didTimeout) {
// On timeout, scan the remaining elements immediately.
scanFromTo(position, Number.POSITIVE_INFINITY);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is OK for now, but not sure the right algorithm. You still want to yield every so often on timeout, I think.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

} else {
// If `requestIdleCallback` isn't available, scan elements in buckets.
// Bucket size is a magic number that fits within a single frame.
const bucketSize = 250;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this just be what happens on timeout of idle callback?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* A default chunkable task.
*/
class Task {
/**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this class and all methods be private?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should state @private is a file-level concept.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/chunk.js Outdated
this.viewer_ = null;

if (viewerOrPromise instanceof Promise) {
viewerOrPromise.then(viewer => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please. Everything here is very subtle.

* A task that's run as part of AMP's startup sequence.
*/
class StartupTask extends Task {
/**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: Can all methods be private?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* @param {ChunkPriority} priority
*/
export function chunk(nodeOrAmpDoc, fn, priority) {
if (deactivated) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you actually want this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need it, but it's better to be consistent if URL contains nochunking=1.

Another option is to change the regex to test for nostartupchunking=1 and remove this.

env.sandbox.stub(resolved, 'then', () => {
throw new Error('No calls expected .then');
const chunks = chunkInstanceForTesting(env.win.document);
env.sandbox.stub(chunks, 'executeASAP_', () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asap_ #styleGuidePolice

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh, I keep forgetting about this rule. Done.

@dreamofabear dreamofabear force-pushed the chunk-scan branch 2 times, most recently from 7f866aa to 63a658e Compare January 20, 2017 01:03

// Helper function for scanning a slice of elements.
const scanFromTo = (start, end) => {
for (let i = start, el = elements[i]; i < end && el; i++) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

el is never updated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, good catch. Fixed with a break which is more readable IMO.

for (let i = start, el = elements[i]; i < end && el; i++) {
const attrs = el.attributes;
const bindings = [];
for (let j = 0; attrs[j]; j++) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

j < attrs.length?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanted to avoid invoking length which is purportedly slow for live lists. Changed to use for..of instead.

// If `requestIdleCallback` is available, scan elements until
// idle time runs out.
if (idleDeadline && !idleDeadline.didTimeout) {
while (idleDeadline.timeRemaining() > 1 && elements[position]) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this elements[index] check weirdness could go away if you made a function that took the element to scan. Then call it from either this while loop, or the above for loop.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought about this. The helper method would need a lot of input params (ugly signature).

I think with the weirdness is reduced with the break and for..of.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just element, evaluatees and boundElements?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's not as bad if extracting the inner loop only; done.

Mutating input params is generally bad for readability but I think it's ok in this case.

const bindings = [];
for (const attr of el.attributes) {
const attrs = el.attributes;
for (let j = 0;; j++) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with your weird for loops? 😛

You could just check j < attrs.length.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha I'm not a fan either. They're in response to: #6967 (comment)

Hard to make everyone happy. :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache the length:

for (let j = 0, l = attributes.length; j < l; j++) {
}

Doing an out-of-bounds access is likely slower than anything else in here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, done.

// If `requestIdleCallback` is available, scan elements until
// idle time runs out.
if (idleDeadline && !idleDeadline.didTimeout) {
while (idleDeadline.timeRemaining() > 1 && elements[position]) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just element, evaluatees and boundElements?

@dreamofabear
Copy link
Copy Markdown
Author

Gentle ping.

Copy link
Copy Markdown
Contributor

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 things:

  1. We should follow up with a TreeWalker implementation
  2. This is the second Queue class in the codebase. Time to extract?
  3. This is the second binary search in the codebase. Time to extract?

Copy link
Copy Markdown
Member

@cramforce cramforce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please hold merge until after canary cut (if it has not happened yet).

@dreamofabear
Copy link
Copy Markdown
Author

Thanks all!

@jridgewell Filed #7230 to track your suggestions.

@dreamofabear dreamofabear merged commit 38afb0d into ampproject:master Jan 27, 2017
@dreamofabear dreamofabear deleted the chunk-scan branch January 27, 2017 18:06
jridgewell pushed a commit to jridgewell/amphtml that referenced this pull request Jan 31, 2017
* Squash chunk-scan commits

* fix lint error

* rename chunk to startupChunk, pass viewer from Chunks to StartupTask

* fix lint and type errors

* support timeRemaining in chunk tasks & misc clean up

* justin's comments

* dima's comments

* malte's comments

* style and lint fix

* fix unit tests

* missed a couple @Private declarations

* fix scan iteration

* remove for..of

* more iteration on iteration

* move element scan to helper method
torch2424 pushed a commit to torch2424/amphtml that referenced this pull request Feb 14, 2017
* Squash chunk-scan commits

* fix lint error

* rename chunk to startupChunk, pass viewer from Chunks to StartupTask

* fix lint and type errors

* support timeRemaining in chunk tasks & misc clean up

* justin's comments

* dima's comments

* malte's comments

* style and lint fix

* fix unit tests

* missed a couple @Private declarations

* fix scan iteration

* remove for..of

* more iteration on iteration

* move element scan to helper method
mrjoro pushed a commit to mrjoro/amphtml that referenced this pull request Apr 28, 2017
* Squash chunk-scan commits

* fix lint error

* rename chunk to startupChunk, pass viewer from Chunks to StartupTask

* fix lint and type errors

* support timeRemaining in chunk tasks & misc clean up

* justin's comments

* dima's comments

* malte's comments

* style and lint fix

* fix unit tests

* missed a couple @Private declarations

* fix scan iteration

* remove for..of

* more iteration on iteration

* move element scan to helper method
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants