Commit 3e43336a authored by Ulan Degenbaev's avatar Ulan Degenbaev Committed by Commit Bot

Update performance.measureMemory to the latest proposal

This changes the result format of the API to the latest version of
the proposal at https://github.com/WICG/performance-measure-memory.

Specifically, per-origin attribution changed to per-frame attribution
with one caveat that cross-origin iframes are considered opaque and
iframes nested in cross-origin iframes do not appear in the result.

The previous version of API:
{
  bytes: 70*MB,
  breakdown: [
    {bytes: 40*MB, globals: 2, type: 'js', origins: ['foo.com']},
    {bytes: 30*MB, globals: 1, type: 'js', origins: ['bar.com']}
  ]
}

The current version of the API:
{
  bytes: 70*MB,
  breakdown: [
    {bytes: 40*MB, type: 'window/js', attribution: ['foo.com']},
    {bytes: 30*MB, type: 'window/js', attribution: ['bar.com']}
  ]
}

Bug: 1049093
Change-Id: I3bbf07ad6e978b9b483561c06cedf6c7a135b7e7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2087627Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Commit-Queue: Ulan Degenbaev <ulan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#747271}
parent 0527aa6f
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// https://github.com/ulan/performance-measure-memory // https://github.com/WICG/performance-measure-memory
// The result of performance.measureMemory(). // The result of performance.measureMemory().
dictionary MeasureMemory { dictionary MeasureMemory {
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
// A single entry of performance.measureMemory() result. // A single entry of performance.measureMemory() result.
dictionary MeasureMemoryBreakdown { dictionary MeasureMemoryBreakdown {
unsigned long long bytes; unsigned long long bytes;
unsigned long long globals; sequence<DOMString> attribution;
sequence<DOMString> origins;
DOMString type; DOMString type;
}; };
...@@ -8,7 +8,11 @@ ...@@ -8,7 +8,11 @@
#include "third_party/blink/renderer/bindings/core/v8/to_v8_for_core.h" #include "third_party/blink/renderer/bindings/core/v8/to_v8_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_measure_memory.h" #include "third_party/blink/renderer/bindings/core/v8/v8_measure_memory.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_measure_memory_breakdown.h" #include "third_party/blink/renderer/bindings/core/v8/v8_measure_memory_breakdown.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/frame.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/page/frame_tree.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h" #include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h" #include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h" #include "third_party/blink/renderer/platform/weborigin/security_origin.h"
...@@ -72,50 +76,90 @@ bool MeasureMemoryDelegate::ShouldMeasure(v8::Local<v8::Context> context) { ...@@ -72,50 +76,90 @@ bool MeasureMemoryDelegate::ShouldMeasure(v8::Local<v8::Context> context) {
namespace { namespace {
// Helper functions for constructing a memory measurement result. // Helper functions for constructing a memory measurement result.
String GetOrigin(v8::Local<v8::Context> context) { const Frame* GetFrame(v8::Local<v8::Context> context) {
ExecutionContext* execution_context = ExecutionContext::From(context); ExecutionContext* execution_context = ExecutionContext::From(context);
if (!execution_context) { if (!execution_context) {
// TODO(ulan): Store URL in v8::Context, so that it is available // The context was detached. Ignore it.
// event for detached contexts. return nullptr;
return String("detached");
} }
const SecurityOrigin* security_origin = DCHECK(execution_context->IsDocument());
execution_context->GetSecurityContext().GetSecurityOrigin(); return Document::From(execution_context)->GetFrame();
return security_origin->ToString();
} }
MeasureMemoryBreakdown* CreateMeasureMemoryBreakdown(size_t bytes, String GetUrl(const Frame* frame) {
size_t globals, // TODO(ulan): Find a way to return the URL at frames open time.
const String& type, const LocalFrame* local_frame = To<LocalFrame>(frame);
const String& origin) { ExecutionContext* execution_context =
MeasureMemoryBreakdown* result = MeasureMemoryBreakdown::Create(); local_frame->GetDocument()->ToExecutionContext();
result->setBytes(bytes); return execution_context->Url().GetString();
result->setGlobals(globals);
result->setType(type);
result->setOrigins(Vector<String>{origin});
return result;
} }
struct BytesAndGlobals { // To avoid information leaks cross-origin iframes are considered opaque for
size_t bytes; // the purposes of attribution. This means the memory of all iframes nested
size_t globals; // in a cross-origin iframe is attributed to the cross-origin iframe.
}; // See https://github.com/WICG/performance-measure-memory for more details.
//
// Given the main frame and the current context, this function walks up the
// tree and finds the topmost cross-origin ancestor frame in the path.
// If that doesn't exist, then all frames in the path are same-origin,
// so the frame corresponding to the current context is returned.
//
// The function returns nullptr if the context was detached.
const Frame* GetAttributionFrame(const Frame* main_frame,
v8::Local<v8::Context> context) {
const Frame* frame = GetFrame(context);
if (!frame) {
// The context was detached. Ignore it.
return nullptr;
}
if (&frame->Tree().Top() != main_frame) {
// This can happen if the frame was detached.
// See the comment in FrameTree::Top().
return nullptr;
}
// Walk up the tree and find the topmost cross-origin ancestor frame.
const Frame* result = frame;
frame = frame->Tree().Parent();
while (frame) {
if (frame->IsCrossOriginToMainFrame())
result = frame;
frame = frame->Tree().Parent();
}
return result;
}
HashMap<String, BytesAndGlobals> GroupByOrigin( // Return per-frame sizes based on the given per-context size.
// TODO(ulan): Revisit this after Origin Trial and see if the results
// are precise enough or if we need to additionally group by JS agent.
HeapHashMap<Member<const Frame>, size_t> GroupByFrame(
const Frame* main_frame,
const std::vector<std::pair<v8::Local<v8::Context>, size_t>>& const std::vector<std::pair<v8::Local<v8::Context>, size_t>>&
context_sizes) { context_sizes) {
HashMap<String, BytesAndGlobals> per_origin; HeapHashMap<Member<const Frame>, size_t> per_frame;
for (const auto& context_size : context_sizes) { for (const auto& context_size : context_sizes) {
const String origin = GetOrigin(context_size.first); const Frame* frame = GetAttributionFrame(main_frame, context_size.first);
auto it = per_origin.find(origin); if (!frame) {
if (it == per_origin.end()) { // The context was detached. Ignore it.
per_origin.insert(origin, BytesAndGlobals{context_size.second, 1}); continue;
}
auto it = per_frame.find(frame);
if (it == per_frame.end()) {
per_frame.insert(frame, context_size.second);
} else { } else {
it->value.bytes += context_size.second; it->value += context_size.second;
++it->value.globals;
} }
} }
return per_origin; return per_frame;
}
MeasureMemoryBreakdown* CreateMeasureMemoryBreakdown(size_t bytes,
const String& type,
const String& url) {
MeasureMemoryBreakdown* result = MeasureMemoryBreakdown::Create();
result->setBytes(bytes);
result->setType(type);
result->setAttribution(url.length() ? Vector<String>{url} : Vector<String>());
return result;
} }
} // anonymous namespace } // anonymous namespace
...@@ -130,11 +174,12 @@ void MeasureMemoryDelegate::MeasurementComplete( ...@@ -130,11 +174,12 @@ void MeasureMemoryDelegate::MeasurementComplete(
return; return;
} }
v8::Local<v8::Context> context = context_.NewLocal(isolate_); v8::Local<v8::Context> context = context_.NewLocal(isolate_);
ExecutionContext* execution_context = ExecutionContext::From(context); const Frame* frame = GetFrame(context);
if (!execution_context) { if (!frame) {
// The context was detached in the meantime. // The context was detached in the meantime.
return; return;
} }
DCHECK(frame->IsMainFrame());
v8::Context::Scope context_scope(context); v8::Context::Scope context_scope(context);
size_t total_size = 0; size_t total_size = 0;
for (const auto& context_size : context_sizes) { for (const auto& context_size : context_sizes) {
...@@ -143,13 +188,19 @@ void MeasureMemoryDelegate::MeasurementComplete( ...@@ -143,13 +188,19 @@ void MeasureMemoryDelegate::MeasurementComplete(
MeasureMemory* result = MeasureMemory::Create(); MeasureMemory* result = MeasureMemory::Create();
result->setBytes(total_size + unattributed_size); result->setBytes(total_size + unattributed_size);
HeapVector<Member<MeasureMemoryBreakdown>> breakdown; HeapVector<Member<MeasureMemoryBreakdown>> breakdown;
HashMap<String, BytesAndGlobals> per_origin(GroupByOrigin(context_sizes)); HeapHashMap<Member<const Frame>, size_t> per_frame(
for (const auto& it : per_origin) { GroupByFrame(frame, context_sizes));
breakdown.push_back(CreateMeasureMemoryBreakdown( size_t attributed_size = 0;
it.value.bytes, it.value.globals, "js", it.key)); for (const auto& it : per_frame) {
attributed_size += it.value;
breakdown.push_back(
CreateMeasureMemoryBreakdown(it.value, "window/js", GetUrl(it.key)));
} }
breakdown.push_back(CreateMeasureMemoryBreakdown( size_t detached_size = total_size - attributed_size;
unattributed_size, context_sizes.size(), "js", "shared")); breakdown.push_back(
CreateMeasureMemoryBreakdown(detached_size, "window/js/detached", ""));
breakdown.push_back(
CreateMeasureMemoryBreakdown(unattributed_size, "window/js/shared", ""));
result->setBreakdown(breakdown); result->setBreakdown(breakdown);
v8::Local<v8::Promise::Resolver> promise_resolver = v8::Local<v8::Promise::Resolver> promise_resolver =
promise_resolver_.NewLocal(isolate_); promise_resolver_.NewLocal(isolate_);
......
...@@ -148,15 +148,36 @@ MemoryInfo* Performance::memory() const { ...@@ -148,15 +148,36 @@ MemoryInfo* Performance::memory() const {
return nullptr; return nullptr;
} }
namespace {
bool IsMeasureMemoryAvailable(ScriptState* script_state) {
// TODO(ulan): We should check for window.crossOriginIsolated when it ships.
// Until then we enable the API only for processes locked to a site
// similar to the precise mode of the legacy performance.memory API.
if (!Platform::Current()->IsLockedToSite()) {
return false;
}
// The window.crossOriginIsolated will be true only for the top-level frame.
// Until the flag is available we check for the top-level condition manually.
ExecutionContext* execution_context = ExecutionContext::From(script_state);
if (!execution_context->IsDocument()) {
return false;
}
LocalFrame* local_frame = Document::From(execution_context)->GetFrame();
if (!local_frame || !local_frame->IsMainFrame()) {
return false;
}
return true;
}
} // anonymous namespace
ScriptPromise Performance::measureMemory( ScriptPromise Performance::measureMemory(
ScriptState* script_state, ScriptState* script_state,
ExceptionState& exception_state) const { ExceptionState& exception_state) const {
if (!Platform::Current()->IsLockedToSite()) { if (!IsMeasureMemoryAvailable(script_state)) {
// TODO(ulan): We should check for COOP and COEP here when they ship.
// Until then we enable the API only for processes locked to a site
// similar to the precise mode of the legacy performance.memory API.
exception_state.ThrowSecurityError( exception_state.ThrowSecurityError(
"Cannot measure memory for cross-origin frames"); "performance.measureMemory is not available in this context");
return ScriptPromise(); return ScriptPromise();
} }
v8::Isolate* isolate = script_state->GetIsolate(); v8::Isolate* isolate = script_state->GetIsolate();
......
...@@ -71,7 +71,7 @@ interface Performance : EventTarget { ...@@ -71,7 +71,7 @@ interface Performance : EventTarget {
// https://groups.google.com/a/chromium.org/d/msg/blink-dev/g5YRCGpC9vs/b4OJz71NmPwJ // https://groups.google.com/a/chromium.org/d/msg/blink-dev/g5YRCGpC9vs/b4OJz71NmPwJ
[Exposed=Window, Measure] readonly attribute MemoryInfo memory; [Exposed=Window, Measure] readonly attribute MemoryInfo memory;
[MeasureAs=MeasureMemory, Exposed=(Window,Worker), CallWith=ScriptState, RuntimeEnabled=MeasureMemory, RaisesException] Promise<MeasureMemory> measureMemory(); [MeasureAs=MeasureMemory, Exposed=Window, CallWith=ScriptState, RuntimeEnabled=MeasureMemory, RaisesException] Promise<MeasureMemory> measureMemory();
// JS Self-Profiling API // JS Self-Profiling API
// https://github.com/WICG/js-self-profiling/ // https://github.com/WICG/js-self-profiling/
......
...@@ -4,5 +4,5 @@ Tests in this directory are for the proposed performance.measureMemory API. ...@@ -4,5 +4,5 @@ Tests in this directory are for the proposed performance.measureMemory API.
This is not yet standardised and browsers should not be expected to pass This is not yet standardised and browsers should not be expected to pass
these tests. these tests.
See the explainer at https://github.com/ulan/performance-measure-memory See the explainer at https://github.com/WICG/performance-measure-memory
for more information about the API. for more information about the API.
// META: script=/common/get-host-info.sub.js // META: script=/common/get-host-info.sub.js
// META: script=./resources/common.js // META: script=./resources/common.js
// META: timeout=long
'use strict'; 'use strict';
promise_test(async testCase => { promise_test(async testCase => {
const frame = document.createElement("iframe"); const frame = document.createElement("iframe");
const path = new URL("resources/iframe.sub.html", window.location).pathname; const child = getUrl(CROSS_ORIGIN, "resources/child.sub.html");
frame.src = `${CROSS_ORIGIN}${path}`; const grandchild = getUrl(CROSS_ORIGIN, "resources/grandchild.sub.html");
frame.src = child;
document.body.append(frame); document.body.append(frame);
try { try {
let result = await performance.measureMemory(); let result = await performance.measureMemory();
checkMeasureMemory(result); checkMeasureMemory(result, {
allowed: [window.location.href, child]
});
} catch (error) { } catch (error) {
if (!(error instanceof DOMException)) { if (!(error instanceof DOMException)) {
throw error; throw error;
......
// META: script=/common/get-host-info.sub.js // META: script=/common/get-host-info.sub.js
// META: script=./resources/common.js // META: script=./resources/common.js
// META: timeout=long
'use strict'; 'use strict';
promise_test(async testCase => { promise_test(async testCase => {
const frame = document.createElement("iframe"); const frame = document.createElement("iframe");
const path = new URL("resources/iframe.sub.html", window.location).pathname; const child = getUrl(SAME_ORIGIN, "resources/child.sub.html");
frame.src = `${SAME_ORIGIN}${path}`; const grandchild = getUrl(SAME_ORIGIN, "resources/grandchild.sub.html");
frame.src = child;
document.body.append(frame); document.body.append(frame);
try { try {
let result = await performance.measureMemory(); let result = await performance.measureMemory();
checkMeasureMemory(result); checkMeasureMemory(result, {
allowed: [window.location.href, child, grandchild],
});
} catch (error) { } catch (error) {
if (!(error instanceof DOMException)) { if (!(error instanceof DOMException)) {
throw error; throw error;
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
promise_test(async testCase => { promise_test(async testCase => {
try { try {
let result = await performance.measureMemory(); let result = await performance.measureMemory();
checkMeasureMemory(result); checkMeasureMemory(result, {allowed: [window.location.href]});
} catch (error) { } catch (error) {
if (!(error instanceof DOMException)) { if (!(error instanceof DOMException)) {
throw error; throw error;
......
<!doctype html>
<meta charset=utf-8>
<html>
<body>
Hello from child iframe.
<iframe src="grandchild.sub.html"></iframe>
</body>
</html>
const SAME_ORIGIN = {origin: get_host_info().HTTPS_ORIGIN, name: "SAME_ORIGIN"}; const SAME_ORIGIN = {origin: get_host_info().HTTPS_ORIGIN, name: "SAME_ORIGIN"};
const CROSS_ORIGIN = {origin: get_host_info().HTTPS_NOTSAMESITE_ORIGIN, name: "CROSS_ORIGIN"} const CROSS_ORIGIN = {origin: get_host_info().HTTPS_NOTSAMESITE_ORIGIN, name: "CROSS_ORIGIN"}
function checkMeasureMemoryBreakdown(breakdown) { function checkMeasureMemoryBreakdown(breakdown, options) {
let allowed = new Set(options.allowed);
assert_own_property(breakdown, 'bytes'); assert_own_property(breakdown, 'bytes');
assert_greater_than_equal(breakdown.bytes, 0); assert_greater_than_equal(breakdown.bytes, 0);
assert_own_property(breakdown, 'globals');
assert_greater_than_equal(breakdown.globals, 0);
assert_own_property(breakdown, 'type'); assert_own_property(breakdown, 'type');
assert_equals(typeof breakdown.type, 'string'); assert_equals(typeof breakdown.type, 'string');
assert_own_property(breakdown, 'origins'); assert_own_property(breakdown, 'attribution');
assert_greater_than_equal(breakdown.origins.length, 1); for (let attribution of breakdown.attribution) {
for (let origin of breakdown.origins) { assert_equals(typeof attribution, 'string');
assert_equals(typeof origin, 'string'); assert_true(
allowed.has(attribution),
`${attribution} must be in ${JSON.stringify(options.allowed)}`);
} }
} }
function checkMeasureMemory(result) { function checkMeasureMemory(result, options) {
assert_own_property(result, 'bytes'); assert_own_property(result, 'bytes');
assert_own_property(result, 'breakdown'); assert_own_property(result, 'breakdown');
let bytes = 0; let bytes = 0;
for (let breakdown of result.breakdown) { for (let breakdown of result.breakdown) {
checkMeasureMemoryBreakdown(breakdown); checkMeasureMemoryBreakdown(breakdown, options);
bytes += breakdown.bytes; bytes += breakdown.bytes;
} }
assert_equals(bytes, result.bytes); assert_equals(bytes, result.bytes);
}
function getUrl(host, relativePath) {
const path = new URL(relativePath, window.location).pathname;
return `${host.origin}/${path}`;
} }
\ No newline at end of file
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
<meta charset=utf-8> <meta charset=utf-8>
<html> <html>
<body> <body>
Hello from iframe. Hello from grandchild iframe.
</body> </body>
</html> </html>
...@@ -1080,7 +1080,6 @@ interface Performance : EventTarget ...@@ -1080,7 +1080,6 @@ interface Performance : EventTarget
method getEntriesByType method getEntriesByType
method mark method mark
method measure method measure
method measureMemory
method now method now
method profile method profile
method setResourceTimingBufferSize method setResourceTimingBufferSize
......
...@@ -1022,7 +1022,6 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -1022,7 +1022,6 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method getEntriesByType [Worker] method getEntriesByType
[Worker] method mark [Worker] method mark
[Worker] method measure [Worker] method measure
[Worker] method measureMemory
[Worker] method now [Worker] method now
[Worker] method profile [Worker] method profile
[Worker] method setResourceTimingBufferSize [Worker] method setResourceTimingBufferSize
......
...@@ -984,7 +984,6 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -984,7 +984,6 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method getEntriesByType [Worker] method getEntriesByType
[Worker] method mark [Worker] method mark
[Worker] method measure [Worker] method measure
[Worker] method measureMemory
[Worker] method now [Worker] method now
[Worker] method profile [Worker] method profile
[Worker] method setResourceTimingBufferSize [Worker] method setResourceTimingBufferSize
......
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