Skip to content

Commit

Permalink
Add more utility functions to v2spa.
Browse files Browse the repository at this point in the history
measureElement and measureText efficiently wrap getBoundingClientRect() to allow
charts to position axes correctly.
sha() wraps crypto.subtle.digest() to speed up session state ids (next CL).
authorizationHeaders() wraps the Google Sign-In library, which is dynamically
fetched by the google-signin component and so is not available for testing.

Bug: catapult:catapult-project#4461
Change-Id: I74431b3a1a249a699885817ac0c4025eb045bffe
Reviewed-on: https://chromium-review.googlesource.com/1214367
Commit-Queue: Ben Hayden <[email protected]>
Reviewed-by: Simon Hatch <[email protected]>
  • Loading branch information
benshayden authored and Commit Bot committed Sep 11, 2018
1 parent 916e932 commit b930111
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 0 deletions.
131 changes: 131 additions & 0 deletions dashboard/dashboard/spa/utils-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,137 @@
assert.strictEqual(FIXTURE, cp.getActiveElement());
});

test('measureElement', async function() {
const rect = await cp.measureElement(FIXTURE);
assert.isBelow(0, rect.bottom);
assert.isBelow(0, rect.height);
assert.isBelow(0, rect.left);
assert.isBelow(0, rect.right);
assert.isBelow(0, rect.top);
assert.isBelow(0, rect.width);
assert.isBelow(0, rect.x);
assert.isBelow(0, rect.y);
});

test('measureText cache', async function() {
assert.strictEqual(await cp.measureText('hello'),
await cp.measureText('hello'));
});

test('measureText', async function() {
const [rect, larger] = await Promise.all([
cp.measureText('hello'),
cp.measureText('hello', {fontSize: 'larger'}),
]);
assert.isBelow(0, rect.height);
assert.isBelow(0, rect.width);
assert.isBelow(rect.height, larger.height);
assert.isBelow(rect.width, larger.width);
});

test('authorizationHeaders empty', async function() {
window.gapi = undefined;
assert.deepEqual([], await cp.authorizationHeaders());

window.gapi = {};
assert.deepEqual([], await cp.authorizationHeaders());

window.gapi = {
auth2: {
getAuthInstance() {
return undefined;
},
},
};
assert.deepEqual([], await cp.authorizationHeaders());

window.gapi = {
auth2: {
getAuthInstance() {
return {
currentUser: {
get() {
return {
getAuthResponse() {
return {};
},
};
}
},
};
},
},
};
assert.deepEqual([], await cp.authorizationHeaders());
window.gapi = undefined;
});

test('authorizationHeaders success', async function() {
window.gapi = {
auth2: {
getAuthInstance() {
return {
currentUser: {
get() {
return {
getAuthResponse() {
return {
expires_at: new Date() + 100,
token_type: 'TYPE',
access_token: 'TOKEN',
};
},
};
}
},
};
},
},
};
assert.deepEqual([['Authorization', 'TYPE TOKEN']],
await cp.authorizationHeaders());
window.gapi = undefined;
});

test('authorizationHeaders reload', async function() {
window.gapi = {
auth2: {
getAuthInstance() {
return {
currentUser: {
get() {
return {
getAuthResponse() {
return {
expires_at: new Date() - 100,
};
},

reloadAuthResponse() {
return {
expires_at: new Date() + 100,
token_type: 'TYPE',
access_token: 'TOKEN',
};
}
};
}
},
};
},
},
};
assert.deepEqual([['Authorization', 'TYPE TOKEN']],
await cp.authorizationHeaders());
window.gapi = undefined;
});

test('sha', async function() {
assert.strictEqual(
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
await cp.sha(''));
});

test('measureTrace', function() {
tr.b.Timing.mark('spa/utils-test', 'measureTrace').end();
tr.b.Timing.mark('spa/utils-test', 'measureTrace').end();
Expand Down
105 changes: 105 additions & 0 deletions dashboard/dashboard/spa/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,79 @@ tr.exportTo('cp', () => {
new Promise(resolve => requestIdleCallback(resolve));
}

async function sha(s) {
s = new TextEncoder('utf-8').encode(s);
const hash = await crypto.subtle.digest('SHA-256', s);
const view = new DataView(hash);
let hex = '';
for (let i = 0; i < view.byteLength; i += 4) {
hex += ('00000000' + view.getUint32(i).toString(16)).slice(-8);
}
return hex;
}

/*
* Returns the bounding rect of the given element.
*/
async function measureElement(element) {
if (!measureElement.READY) {
measureElement.READY = cp.animationFrame().then(() => {
measureElement.READY = undefined;
});
}
await measureElement.READY;
return element.getBoundingClientRect();
}

// measureText() below takes a string and optional style options, renders the
// text in this div, and returns the size of the text. This div helps
// measureText() render its arguments invisibly.
const MEASURE_TEXT_HOST = document.createElement('div');
MEASURE_TEXT_HOST.style.position = 'fixed';
MEASURE_TEXT_HOST.style.visibility = 'hidden';
MEASURE_TEXT_HOST.style.zIndex = -1000;
window.addEventListener('load', () =>
document.body.appendChild(MEASURE_TEXT_HOST));

// Assuming the computed style of MEASURE_TEXT_HOST doesn't change, measuring
// a string with the same options should always return the same size, so the
// measurements can be memoized. Also, measuring text is asynchronous, so this
// cache can store promises in case callers try to measure the same text twice
// in the same frame.
const MEASURE_TEXT_CACHE = new Map();
const MAX_MEASURE_TEXT_CACHE_SIZE = 1000;

/*
* Returns the bounding rect of the given textContent after applying the given
* opt_options to a <span> containing textContent.
*/
async function measureText(textContent, opt_options) {
let cacheKey = {textContent, ...opt_options};
cacheKey = JSON.stringify(cacheKey, Object.keys(cacheKey).sort());
if (MEASURE_TEXT_CACHE.has(cacheKey)) {
return await MEASURE_TEXT_CACHE.get(cacheKey);
}

const span = document.createElement('span');
span.style.whiteSpace = 'nowrap';
span.style.display = 'inline-block';
span.textContent = textContent;
Object.assign(span.style, opt_options);
MEASURE_TEXT_HOST.appendChild(span);

const promise = cp.measureElement(span).then(({width, height}) => {
return {width, height};
});
while (MEASURE_TEXT_CACHE.size > MAX_MEASURE_TEXT_CACHE_SIZE) {
MEASURE_TEXT_CACHE.delete(MEASURE_TEXT_CACHE.keys().next().value);
}
MEASURE_TEXT_CACHE.set(cacheKey, promise);
const rect = await promise;
MEASURE_TEXT_CACHE.set(cacheKey, rect);
MEASURE_TEXT_HOST.removeChild(span);
return rect;
}

function measureTrace() {
const events = [];
const loadTimes = Object.entries(performance.timing.toJSON()).filter(p =>
Expand Down Expand Up @@ -206,19 +279,51 @@ tr.exportTo('cp', () => {
return state;
}

/**
* Wrap Google Sign-in client library to build the Authorization header, if
* one is available. Automatically reloads the token if necessary.
*/
async function authorizationHeaders() {
if (window.gapi === undefined) return [];
if (gapi.auth2 === undefined) return [];

const auth = gapi.auth2.getAuthInstance();
if (!auth) return [];
const user = auth.currentUser.get();
let response = user.getAuthResponse();

if (response.expires_at === undefined) {
// The user is not signed in.
return [];
}

if (response.expires_at < new Date()) {
// The token has expired, so reload it.
response = await user.reloadAuthResponse();
}

return [
['Authorization', response.token_type + ' ' + response.access_token],
];
}

return {
afterRender,
animationFrame,
authorizationHeaders,
buildProperties,
buildState,
deepFreeze,
getActiveElement,
idle,
isElementChildOf,
measureElement,
measureHistograms,
measureTable,
measureText,
measureTrace,
setImmutable,
sha,
timeout,
};
});

0 comments on commit b930111

Please sign in to comment.