Commit 196c3f26 authored by Joe Mason's avatar Joe Mason Committed by Chromium LUCI CQ

[PM] Add WebMemoryAggregator class and tests

A follow-up will use this to fill in results for WebMeasureMemory.

Bug: 1085129
Change-Id: If3c4ff777b4e4ae80f7a695c42398ea58f7ef2cc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2557730
Commit-Queue: Joe Mason <joenotcharles@chromium.org>
Reviewed-by: default avatarNicolás Peña Moreno <npm@chromium.org>
Reviewed-by: default avatarUlan Degenbaev <ulan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#835232}
parent bfb5e2c0
...@@ -28,18 +28,23 @@ module performance_manager.mojom; ...@@ -28,18 +28,23 @@ module performance_manager.mojom;
// Information about ExecutionContext reported in the memory usage breakdown. // Information about ExecutionContext reported in the memory usage breakdown.
struct WebMemoryAttribution { struct WebMemoryAttribution {
// Specifies the scope (or type) of the context. // Specifies the scope (or type) of the context.
// The invariant mentioned above ensures that cross-origin contexts have
// type kWindow corresponding to iframes and that information is already
// known to the calling origin because it created the iframe.
enum Scope { enum Scope {
kCrossOriginAggregated, // Dummy scope for cross-origin iframes.
kWindow, kWindow,
// TODO(1085129): Add worker scopes once they are implemented. // TODO(1085129): Add worker scopes once they are implemented.
}; };
Scope scope; Scope scope;
// The current URL of the context. It is null for cross-origin contexts. // The current URL of the context. It is null for cross-origin contexts.
// This is a string instead of url.mojom.Url because it is only used for
// reporting so there's no need to serialize to GURL, which has a lot of
// overhead.
string? url; string? url;
// The src attribute of the container. Can be null if the container has no
// TODO(1085129): Add src and id fields once they are implemented. // such attribute.
string? src;
// The id attribute of the container. Can be null if the container has no
// such attribute.
string? id;
}; };
// Describes a memory region and attributes it to a set of contexts. // Describes a memory region and attributes it to a set of contexts.
......
...@@ -5,12 +5,18 @@ ...@@ -5,12 +5,18 @@
#include "components/performance_manager/v8_memory/web_memory_aggregator.h" #include "components/performance_manager/v8_memory/web_memory_aggregator.h"
#include <memory> #include <memory>
#include <utility>
#include <vector>
#include "base/bind.h" #include "base/bind.h"
#include "base/callback.h" #include "base/callback.h"
#include "base/check.h"
#include "base/optional.h"
#include "components/performance_manager/public/graph/frame_node.h" #include "components/performance_manager/public/graph/frame_node.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/graph/process_node.h" #include "components/performance_manager/public/graph/process_node.h"
#include "components/performance_manager/public/v8_memory/web_memory.h" #include "components/performance_manager/public/v8_memory/web_memory.h"
#include "components/performance_manager/v8_memory/v8_context_tracker.h"
#include "url/gurl.h" #include "url/gurl.h"
namespace performance_manager { namespace performance_manager {
...@@ -19,6 +25,8 @@ namespace v8_memory { ...@@ -19,6 +25,8 @@ namespace v8_memory {
namespace { namespace {
using AttributionScope = mojom::WebMemoryAttribution::Scope;
mojom::WebMemoryMeasurementPtr BuildMemoryUsageResult( mojom::WebMemoryMeasurementPtr BuildMemoryUsageResult(
const blink::LocalFrameToken& frame_token, const blink::LocalFrameToken& frame_token,
const ProcessNode* process_node) { const ProcessNode* process_node) {
...@@ -77,8 +85,239 @@ WebMeasurementModeToRequestMeasurementMode( ...@@ -77,8 +85,239 @@ WebMeasurementModeToRequestMeasurementMode(
} }
} }
// Returns true if |page_node| has an opener that should be followed by the
// aggregation algorithm.
bool ShouldFollowOpenerLink(const PageNode* page_node) {
return page_node->GetOpenedType() == PageNode::OpenedType::kPopup;
}
// Returns |frame_node|'s origin based on its current url.
url::Origin GetOrigin(const FrameNode* frame_node) {
return url::Origin::Create(frame_node->GetURL());
}
// Returns the parent of |frame_node|, the opener if it has no parent, or
// nullptr if it has neither.
const FrameNode* GetParentOrOpener(const FrameNode* frame_node) {
// Only the main frame of a page should have an opener. So first check if
// there's a parent and, if not, check if there's an opener.
if (auto* parent = frame_node->GetParentFrameNode())
return parent;
auto* page_node = frame_node->GetPageNode();
DCHECK(page_node);
if (ShouldFollowOpenerLink(page_node))
return page_node->GetOpenerFrameNode();
return nullptr;
}
// Returns a mutable pointer to the WebMemoryAttribution structure in the given
// |breakdown|.
mojom::WebMemoryAttribution* GetAttributionFromBreakdown(
mojom::WebMemoryBreakdownEntry* breakdown) {
// We only store a single attribution with each breakdown.
DCHECK_EQ(breakdown->attribution.size(), 1U);
mojom::WebMemoryAttribution* attribution =
breakdown->attribution.front().get();
DCHECK(attribution);
return attribution;
}
// Returns a const pointer to the WebMemoryAttribution structure in the given
// |breakdown|.
const mojom::WebMemoryAttribution* GetAttributionFromBreakdown(
const mojom::WebMemoryBreakdownEntry* breakdown) {
auto* mutable_breakdown =
const_cast<mojom::WebMemoryBreakdownEntry*>(breakdown);
auto* mutable_attribution = GetAttributionFromBreakdown(mutable_breakdown);
return const_cast<mojom::WebMemoryAttribution*>(mutable_attribution);
}
} // anonymous namespace } // anonymous namespace
////////////////////////////////////////////////////////////////////////////////
// WebMemoryMeasurer
WebMemoryMeasurer::WebMemoryMeasurer(
const blink::LocalFrameToken& frame_token,
V8DetailedMemoryRequest::MeasurementMode mode,
MeasurementCallback callback)
: frame_token_(frame_token),
callback_(std::move(callback)),
request_(std::make_unique<V8DetailedMemoryRequestOneShot>(mode)) {}
WebMemoryMeasurer::~WebMemoryMeasurer() = default;
void WebMemoryMeasurer::MeasurementComplete(
const ProcessNode* process_node,
const V8DetailedMemoryProcessData*) {
// TODO(crbug.com/1085129): Call AggregateMemoryResult here instead of
// BuildMemoryUsageResult.
std::move(callback_).Run(BuildMemoryUsageResult(frame_token_, process_node));
}
////////////////////////////////////////////////////////////////////////////////
// WebMemoryAggregator
WebMemoryAggregator::WebMemoryAggregator(const FrameNode* requesting_node)
: requesting_origin_(GetOrigin(requesting_node)),
aggregation_start_node_(
internal::FindAggregationStartNode(requesting_node)) {
DCHECK(aggregation_start_node_);
}
WebMemoryAggregator::~WebMemoryAggregator() = default;
WebMemoryAggregator::NodeAggregationType
WebMemoryAggregator::FindNodeAggregationType(const FrameNode* frame_node) {
#if DCHECK_IS_ON()
auto* node = frame_node;
while (node && node != aggregation_start_node_) {
node = GetParentOrOpener(node);
}
// Should have broken out of the loop by reaching the start node, not nullptr.
DCHECK_EQ(node, aggregation_start_node_);
#endif
// If |frame_node| is in a different browsing context group from |start_node|
// it should be invisible.
if (frame_node->GetBrowsingInstanceId() !=
aggregation_start_node_->GetBrowsingInstanceId()) {
return NodeAggregationType::kInvisible;
}
// If |frame_node| is same-origin to |start_node|, it's an aggregation point.
// (This trivially includes the |start_node| itself.)
if (requesting_origin_.IsSameOriginWith(GetOrigin(frame_node)))
return NodeAggregationType::kSameOriginAggregationPoint;
DCHECK_NE(frame_node, aggregation_start_node_);
// If |frame_node| is cross-origin from |start_node|, but is a direct child of
// a same-origin node, its existence is visible to |start_node| so it's an
// aggregation point. But its current url will be hidden from |start_node|.
const FrameNode* parent_node = frame_node->GetParentFrameNode();
// |frame_node| is a child of |start_node| so must have a parent.
DCHECK(parent_node);
if (requesting_origin_.IsSameOriginWith(GetOrigin(parent_node)))
return NodeAggregationType::kCrossOriginAggregationPoint;
// Otherwise |frame_node|'s memory should be aggregated into the last
// aggregation point.
return NodeAggregationType::kCrossOriginAggregated;
}
mojom::WebMemoryMeasurementPtr
WebMemoryAggregator::AggregateMeasureMemoryResult() {
aggregation_result_ = mojom::WebMemoryMeasurement::New();
VisitFrame(nullptr, aggregation_start_node_);
// Don't report breakdowns without any memory use.
base::EraseIf(aggregation_result_->breakdown,
[](const mojom::WebMemoryBreakdownEntryPtr& entry) {
return entry->bytes == 0;
});
return std::move(aggregation_result_);
}
bool WebMemoryAggregator::VisitFrame(
mojom::WebMemoryBreakdownEntry* enclosing_aggregation_point,
const FrameNode* frame_node) {
DCHECK(aggregation_result_);
DCHECK(enclosing_aggregation_point || frame_node == aggregation_start_node_);
DCHECK(frame_node);
// An aggregation point is a node in the graph that holds a memory breakdown
// covering itself and any descendant nodes that are aggregated into the same
// breakdown. It is represented directly by the WebMemoryBreakdownEntry
// object the describes the breakdown since there is no extra information to
// store about the aggregation point.
mojom::WebMemoryBreakdownEntry* aggregation_point = nullptr;
switch (FindNodeAggregationType(frame_node)) {
case NodeAggregationType::kInvisible:
// Ignore this node, continue iterating its siblings.
return true;
case NodeAggregationType::kSameOriginAggregationPoint:
// Create a new aggregation point with window scope. Since this node is
// same-origin to the start node, the start node can view its current
// url.
aggregation_point = internal::CreateBreakdownEntry(
AttributionScope::kWindow, frame_node->GetURL().spec(),
aggregation_result_.get());
if (frame_node->IsMainFrame() || frame_node == aggregation_start_node_) {
// There should be no id or src attribute since there is no visible
// parent to take them from. Do nothing.
} else if (internal::GetSameOriginParentOrOpener(frame_node,
requesting_origin_)) {
// The parent or opener is also same-origin so the start node can view
// its attributes. Add the id and src recorded for the node in
// V8ContextTracker to the new breakdown entry.
internal::SetBreakdownAttributionFromFrame(frame_node,
aggregation_point);
} else {
// Some grandparent node is the most recent aggregation point whose
// attributes are visible to the start node, and
// |enclosing_aggregation_point| includes those attributes. Copy the id
// and src attributes from there.
internal::CopyBreakdownAttribution(enclosing_aggregation_point,
aggregation_point);
}
break;
case NodeAggregationType::kCrossOriginAggregationPoint:
// Create a new aggregation point with cross-origin-aggregated scope.
// Since this node is NOT same-origin to the start node, the start node
// CANNOT view its current url.
aggregation_point = internal::CreateBreakdownEntry(
AttributionScope::kCrossOriginAggregated, base::nullopt,
aggregation_result_.get());
// This is cross-origin but not being aggregated into another aggregation
// point, so its parent or opener must be same-origin to the start node,
// which can therefore view its attributes. Add the id and src recorded
// for the node in V8ContextTracker to the new breakdown entry.
internal::SetBreakdownAttributionFromFrame(frame_node, aggregation_point);
break;
case NodeAggregationType::kCrossOriginAggregated:
// Update the enclosing aggregation point in-place.
aggregation_point = enclosing_aggregation_point;
break;
}
// Now update the memory used in the chosen aggregation point.
DCHECK(aggregation_point);
if (auto* frame_data =
V8DetailedMemoryExecutionContextData::ForFrameNode(frame_node)) {
aggregation_point->bytes += frame_data->v8_bytes_used();
}
// Recurse into children and opened pages. This node's aggregation point
// becomes the enclosing aggregation point for those nodes. Unretained is safe
// because the Visit* functions are synchronous.
frame_node->VisitOpenedPageNodes(
base::BindRepeating(&WebMemoryAggregator::VisitOpenedPage,
base::Unretained(this), aggregation_point));
return frame_node->VisitChildFrameNodes(
base::BindRepeating(&WebMemoryAggregator::VisitFrame,
base::Unretained(this), aggregation_point));
}
bool WebMemoryAggregator::VisitOpenedPage(
mojom::WebMemoryBreakdownEntry* enclosing_aggregation_point,
const PageNode* page_node) {
if (ShouldFollowOpenerLink(page_node)) {
// Visit only the "current" main frame instead of all of the main frames
// (non-current ones are either about to die, or represent an ongoing
// navigation).
return VisitFrame(enclosing_aggregation_point,
page_node->GetMainFrameNode());
}
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Free functions
// Implements the public function in public/v8_memory/web_memory.h // Implements the public function in public/v8_memory/web_memory.h
void WebMeasureMemory( void WebMeasureMemory(
const FrameNode* frame_node, const FrameNode* frame_node,
...@@ -99,22 +338,84 @@ void WebMeasureMemory( ...@@ -99,22 +338,84 @@ void WebMeasureMemory(
std::move(measurement_complete_callback)); std::move(measurement_complete_callback));
} }
WebMemoryMeasurer::WebMemoryMeasurer( namespace internal {
const blink::LocalFrameToken& frame_token,
V8DetailedMemoryRequest::MeasurementMode mode,
MeasurementCallback callback)
: frame_token_(frame_token),
callback_(std::move(callback)),
request_(std::make_unique<V8DetailedMemoryRequestOneShot>(mode)) {}
WebMemoryMeasurer::~WebMemoryMeasurer() = default; const FrameNode* GetSameOriginParentOrOpener(const FrameNode* frame_node,
const url::Origin& origin) {
if (auto* parent_or_opener = GetParentOrOpener(frame_node)) {
if (origin.IsSameOriginWith(GetOrigin(parent_or_opener)))
return parent_or_opener;
}
return nullptr;
}
void WebMemoryMeasurer::MeasurementComplete( const FrameNode* FindAggregationStartNode(const FrameNode* requesting_node) {
const ProcessNode* process_node, DCHECK(requesting_node);
const V8DetailedMemoryProcessData*) { auto requesting_origin = GetOrigin(requesting_node);
std::move(callback_).Run(BuildMemoryUsageResult(frame_token_, process_node));
// Follow parent and opener links to find the most general same-site node to
// start the aggregation traversal from.
const FrameNode* start_node = requesting_node;
while (auto* parent_or_opener =
GetSameOriginParentOrOpener(start_node, requesting_origin)) {
start_node = parent_or_opener;
}
DCHECK(start_node);
DCHECK(requesting_origin.IsSameOriginWith(GetOrigin(start_node)));
// Make sure we didn't break out of the browsing context group.
DCHECK_EQ(start_node->GetBrowsingInstanceId(),
requesting_node->GetBrowsingInstanceId());
return start_node;
}
mojom::WebMemoryBreakdownEntry* CreateBreakdownEntry(
AttributionScope scope,
base::Optional<std::string> url,
mojom::WebMemoryMeasurement* measurement) {
auto breakdown = mojom::WebMemoryBreakdownEntry::New();
auto attribution = mojom::WebMemoryAttribution::New();
attribution->scope = scope;
attribution->url = std::move(url);
breakdown->attribution.push_back(std::move(attribution));
measurement->breakdown.push_back(std::move(breakdown));
return measurement->breakdown.back().get();
}
void SetBreakdownAttributionFromFrame(
const FrameNode* frame_node,
mojom::WebMemoryBreakdownEntry* breakdown) {
DCHECK(breakdown);
DCHECK(frame_node);
auto* v8_context_tracker =
V8ContextTracker::GetFromGraph(frame_node->GetGraph());
DCHECK(v8_context_tracker);
auto* ec_state =
v8_context_tracker->GetExecutionContextState(frame_node->GetFrameToken());
if (!ec_state)
return;
const mojom::IframeAttributionDataPtr& ec_attribution =
ec_state->iframe_attribution_data;
if (!ec_attribution)
return;
auto* attribution = GetAttributionFromBreakdown(breakdown);
attribution->id = ec_attribution->id;
attribution->src = ec_attribution->src;
} }
void CopyBreakdownAttribution(const mojom::WebMemoryBreakdownEntry* from,
mojom::WebMemoryBreakdownEntry* to) {
DCHECK(from);
DCHECK(to);
const auto* from_attribution = GetAttributionFromBreakdown(from);
auto* to_attribution = GetAttributionFromBreakdown(to);
to_attribution->id = from_attribution->id;
to_attribution->src = from_attribution->src;
}
} // namespace internal
} // namespace v8_memory } // namespace v8_memory
} // namespace performance_manager } // namespace performance_manager
...@@ -11,9 +11,11 @@ ...@@ -11,9 +11,11 @@
#include "components/performance_manager/public/mojom/web_memory.mojom.h" #include "components/performance_manager/public/mojom/web_memory.mojom.h"
#include "components/performance_manager/public/v8_memory/v8_detailed_memory.h" #include "components/performance_manager/public/v8_memory/v8_detailed_memory.h"
#include "third_party/blink/public/common/tokens/tokens.h" #include "third_party/blink/public/common/tokens/tokens.h"
#include "url/origin.h"
namespace performance_manager { namespace performance_manager {
class FrameNode;
class ProcessNode; class ProcessNode;
namespace v8_memory { namespace v8_memory {
...@@ -21,6 +23,8 @@ namespace v8_memory { ...@@ -21,6 +23,8 @@ namespace v8_memory {
// A helper class for implementing WebMeasureMemory(). This manages a request // A helper class for implementing WebMeasureMemory(). This manages a request
// object that sends a V8 detailed memory request to the renderer, and formats // object that sends a V8 detailed memory request to the renderer, and formats
// the result into a mojom::WebMemoryMeasurement. // the result into a mojom::WebMemoryMeasurement.
// TODO(crbug.com/1085129): Extend this to measure all renderers that are
// reachable from the requesting node.
class WebMemoryMeasurer { class WebMemoryMeasurer {
public: public:
using MeasurementCallback = using MeasurementCallback =
...@@ -29,9 +33,11 @@ class WebMemoryMeasurer { ...@@ -29,9 +33,11 @@ class WebMemoryMeasurer {
WebMemoryMeasurer(const blink::LocalFrameToken&, WebMemoryMeasurer(const blink::LocalFrameToken&,
V8DetailedMemoryRequest::MeasurementMode, V8DetailedMemoryRequest::MeasurementMode,
MeasurementCallback); MeasurementCallback);
~WebMemoryMeasurer(); ~WebMemoryMeasurer();
WebMemoryMeasurer(const WebMemoryMeasurer& other) = delete;
WebMemoryMeasurer& operator=(const WebMemoryMeasurer& other) = delete;
V8DetailedMemoryRequestOneShot* request() const { return request_.get(); } V8DetailedMemoryRequestOneShot* request() const { return request_.get(); }
// A callback for V8DetailedMemoryRequestOneShot. // A callback for V8DetailedMemoryRequestOneShot.
...@@ -44,6 +50,119 @@ class WebMemoryMeasurer { ...@@ -44,6 +50,119 @@ class WebMemoryMeasurer {
std::unique_ptr<V8DetailedMemoryRequestOneShot> request_; std::unique_ptr<V8DetailedMemoryRequestOneShot> request_;
}; };
// Traverses the graph of execution contexts to find the results of the last
// memory measurement and aggregates them according to the rules defined in
// https://wicg.github.io/performance-measure-memory. (This implements the
// draft of 20 October 2020.)
class WebMemoryAggregator {
public:
// Constructs an aggregator for the results of a memory request from
// |requesting_node|. This expects the caller to check if |requesting_node|
// is allowed to measure memory according to the spec.
//
// The aggregation is performed by calling AggregateMemoryResult. The graph
// traversal will not start directly from |requesting_node|, but from the
// highest node in the frame tree that is visible to it as found by
// FindAggregationStartNode. (This allows a same-origin subframe to request
// memory for the whole page it's embedded in.)
explicit WebMemoryAggregator(const FrameNode* requesting_node);
~WebMemoryAggregator();
WebMemoryAggregator(const WebMemoryAggregator& other) = delete;
WebMemoryAggregator& operator=(const WebMemoryAggregator& other) = delete;
// The various ways a node can be treated during the aggregation.
enum class NodeAggregationType {
// Node is same-origin to |requesting_node|; will be a new aggregation
// point with scope "Window".
kSameOriginAggregationPoint,
// Node is cross-origin with |requesting_node| but its parent is not; will
// be a new aggregation point with scope
// "cross-origin-aggregated".
kCrossOriginAggregationPoint,
// Node is cross-origin with |requesting_node| and so is its parent; will
// be aggregated into its parent's aggregation point.
kCrossOriginAggregated,
// Node is in a different browsing context group; will not be added to the
// aggregation.
kInvisible,
};
// Returns the origin of |requesting_node|.
const url::Origin& requesting_origin() const { return requesting_origin_; }
// Returns the way that |frame_node| should be treated during the
// aggregation. |aggregation_start_node_| must be reachable from
// |frame_node| by following parent/child or opener links. This will always
// be true if |frame_node| comes from a call to VisitFrame.
NodeAggregationType FindNodeAggregationType(const FrameNode* frame_node);
// Performs the aggregation.
mojom::WebMemoryMeasurementPtr AggregateMeasureMemoryResult();
private:
// FrameNodeVisitor that recursively adds |frame_node| and its children to
// the aggregation. |enclosing_aggregation_point| is the aggregation point
// that |frame_node|'s parent or opener is in. Always returns true to
// continue traversal.
bool VisitFrame(mojom::WebMemoryBreakdownEntry* enclosing_aggregation_point,
const FrameNode* frame_node);
// PageNodeVisitor that recursively adds |page_node|'s main frames and their
// children to the aggregation. |enclosing_aggregation_point| is the
// aggregation point that |page_node|'s opener is in. Always returns true to
// continue traversal.
bool VisitOpenedPage(
mojom::WebMemoryBreakdownEntry* enclosing_aggregation_point,
const PageNode* page_node);
// The origin of |requesting_node|. Cached so it doesn't have to be
// recalculated in each call to VisitFrame.
const url::Origin requesting_origin_;
// The node that the graph traversal should start from, found from
// |requesting_node| using FindAggregationStartNode.
const FrameNode* aggregation_start_node_;
// Stores the result of the aggregation. This is populated by
// AggregateMeasureMemoryResult.
mojom::WebMemoryMeasurementPtr aggregation_result_;
};
namespace internal {
// These functions are used in the implementation and exposed in the header for
// testing.
// Returns |frame_node|'s parent or opener if the parent or opener is
// same-origin with |origin|, nullptr otherwise.
const FrameNode* GetSameOriginParentOrOpener(const FrameNode* frame_node,
const url::Origin& origin);
// Walks back the chain of parents and openers from |requesting_node| to find
// the farthest ancestor that should be visible to it (all intermediate nodes
// in the chain are same-origin).
const FrameNode* FindAggregationStartNode(const FrameNode* requesting_node);
// Creates a new breakdown entry with the given |scope| and |url|, and adds it
// to the list in |measurement|. Returns a pointer to the newly created entry.
mojom::WebMemoryBreakdownEntry* CreateBreakdownEntry(
mojom::WebMemoryAttribution::Scope scope,
base::Optional<std::string> url,
mojom::WebMemoryMeasurement* measurement);
// Sets the id and src attributes of |breakdown| using those stored in the
// V8ContextTracker for the given |frame_node|.
void SetBreakdownAttributionFromFrame(
const FrameNode* frame_node,
mojom::WebMemoryBreakdownEntry* breakdown);
// Copies the id and src attributes from |from| to |to|.
void CopyBreakdownAttribution(const mojom::WebMemoryBreakdownEntry* from,
mojom::WebMemoryBreakdownEntry* to);
} // namespace internal
} // namespace v8_memory } // namespace v8_memory
} // namespace performance_manager } // namespace performance_manager
......
...@@ -4,17 +4,26 @@ ...@@ -4,17 +4,26 @@
#include "components/performance_manager/v8_memory/web_memory_aggregator.h" #include "components/performance_manager/v8_memory/web_memory_aggregator.h"
#include <algorithm>
#include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "base/bind.h"
#include "base/check.h"
#include "base/containers/flat_map.h" #include "base/containers/flat_map.h"
#include "base/memory/weak_ptr.h" #include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/test/bind.h" #include "base/test/bind.h"
#include "base/test/task_environment.h" #include "base/test/task_environment.h"
#include "base/trace_event/traced_value.h"
#include "components/performance_manager/graph/frame_node_impl.h" #include "components/performance_manager/graph/frame_node_impl.h"
#include "components/performance_manager/graph/page_node_impl.h" #include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h" #include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/mojom/v8_contexts.mojom.h"
#include "components/performance_manager/public/mojom/web_memory.mojom.h"
#include "components/performance_manager/public/performance_manager.h" #include "components/performance_manager/public/performance_manager.h"
#include "components/performance_manager/public/v8_memory/v8_detailed_memory.h" #include "components/performance_manager/public/v8_memory/v8_detailed_memory.h"
#include "components/performance_manager/public/v8_memory/web_memory.h" #include "components/performance_manager/public/v8_memory/web_memory.h"
...@@ -29,17 +38,15 @@ namespace performance_manager { ...@@ -29,17 +38,15 @@ namespace performance_manager {
namespace v8_memory { namespace v8_memory {
using AttributionScope = mojom::WebMemoryAttribution::Scope;
using NodeAggregationType = WebMemoryAggregator::NodeAggregationType;
using WebMemoryAggregatorPMTest = V8MemoryPerformanceManagerTestHarness; using WebMemoryAggregatorPMTest = V8MemoryPerformanceManagerTestHarness;
class WebMemoryAggregatorTest : public GraphTestHarness { class WebMemoryAggregatorTest : public GraphTestHarness {
public: public:
using Super = GraphTestHarness; using Super = GraphTestHarness;
// Wrapper for the browsing instance id to improve test readability.
struct BrowsingInstance {
int id;
};
// Wrapper for memory usage bytes to improve test readability. // Wrapper for memory usage bytes to improve test readability.
struct Bytes { struct Bytes {
uint64_t bytes; uint64_t bytes;
...@@ -49,10 +56,45 @@ class WebMemoryAggregatorTest : public GraphTestHarness { ...@@ -49,10 +56,45 @@ class WebMemoryAggregatorTest : public GraphTestHarness {
void SetUp() override; void SetUp() override;
// Creates and adds a new frame node to the graph. // Creates and adds a new frame node to the graph.
FrameNodeImpl* AddFrameNode(std::string url, FrameNodeImpl* AddFrameNode(
BrowsingInstance, std::string url,
Bytes, Bytes bytes,
FrameNodeImpl* parent = nullptr); FrameNodeImpl* parent = nullptr,
base::Optional<std::string> id_attribute = base::nullopt,
base::Optional<std::string> src_attribute = base::nullopt) {
return AddFrameNodeImpl(url, kDefaultBrowsingInstanceId, bytes, parent,
/*opener=*/nullptr, id_attribute, src_attribute);
}
// Creates a frame node as if from window.open and adds it to the graph.
FrameNodeImpl* AddFrameNodeFromOpener(std::string url,
Bytes bytes,
FrameNodeImpl* opener) {
return AddFrameNodeImpl(url, kDefaultBrowsingInstanceId, bytes,
/*parent=*/nullptr, opener);
}
// Creates a frame node in a different browsing instance and adds it to the
// graph.
FrameNodeImpl* AddCrossBrowsingInstanceFrameNode(
std::string url,
Bytes bytes,
FrameNodeImpl* parent = nullptr,
base::Optional<std::string> id_attribute = base::nullopt,
base::Optional<std::string> src_attribute = base::nullopt) {
return AddFrameNodeImpl(url, kDefaultBrowsingInstanceId + 1, bytes, parent,
/*opener=*/nullptr, id_attribute, src_attribute);
}
// Creates a frame node in a different browsing instance as if from
// window.open and adds it to the graph.
FrameNodeImpl* AddCrossBrowsingInstanceFrameNodeFromOpener(
std::string url,
Bytes bytes,
FrameNodeImpl* opener) {
return AddFrameNodeImpl(url, kDefaultBrowsingInstanceId + 1, bytes,
/*parent=*/nullptr, opener);
}
// Invokes memory measurement and verifies that the result matches the // Invokes memory measurement and verifies that the result matches the
// expected memory usage that is provided as a table from a frame URL to // expected memory usage that is provided as a table from a frame URL to
...@@ -61,39 +103,89 @@ class WebMemoryAggregatorTest : public GraphTestHarness { ...@@ -61,39 +103,89 @@ class WebMemoryAggregatorTest : public GraphTestHarness {
base::flat_map<std::string, Bytes> expected); base::flat_map<std::string, Bytes> expected);
private: private:
static constexpr int kDefaultBrowsingInstanceId = 0;
// Creates and adds a new frame node to the graph.
FrameNodeImpl* AddFrameNodeImpl(
std::string url,
int browsing_instance_id,
Bytes bytes,
FrameNodeImpl* parent = nullptr,
FrameNodeImpl* opener = nullptr,
base::Optional<std::string> id_attribute = base::nullopt,
base::Optional<std::string> src_attribute = base::nullopt);
int GetNextUniqueId(); int GetNextUniqueId();
TestNodeWrapper<ProcessNodeImpl> process_; TestNodeWrapper<ProcessNodeImpl> process_;
TestNodeWrapper<PageNodeImpl> page_; std::vector<TestNodeWrapper<PageNodeImpl>> pages_;
std::vector<TestNodeWrapper<FrameNodeImpl>> frames_; std::vector<TestNodeWrapper<FrameNodeImpl>> frames_;
int next_unique_id_ = 0; int next_unique_id_ = 0;
}; };
void WebMemoryAggregatorTest::SetUp() { void WebMemoryAggregatorTest::SetUp() {
GetGraphFeaturesHelper().EnableExecutionContextRegistry(); GetGraphFeaturesHelper().EnableV8ContextTracker();
Super::SetUp(); Super::SetUp();
process_ = CreateNode<ProcessNodeImpl>(); process_ = CreateNode<ProcessNodeImpl>();
page_ = CreateNode<PageNodeImpl>(); pages_.push_back(CreateNode<PageNodeImpl>());
} }
int WebMemoryAggregatorTest::GetNextUniqueId() { int WebMemoryAggregatorTest::GetNextUniqueId() {
return next_unique_id_++; return next_unique_id_++;
} }
FrameNodeImpl* WebMemoryAggregatorTest::AddFrameNode( FrameNodeImpl* WebMemoryAggregatorTest::AddFrameNodeImpl(
std::string url, std::string url,
BrowsingInstance browsing_instance_id, int browsing_instance_id,
Bytes memory_usage, Bytes memory_usage,
FrameNodeImpl* parent) { FrameNodeImpl* parent,
FrameNodeImpl* opener,
base::Optional<std::string> id_attribute,
base::Optional<std::string> src_attribute) {
// If there's an opener, the new frame is also a new page.
auto* page = pages_.front().get();
if (opener) {
pages_.push_back(CreateNode<PageNodeImpl>());
page = pages_.back().get();
page->SetOpenerFrameNodeAndOpenedType(opener, PageNode::OpenedType::kPopup);
}
int frame_tree_node_id = GetNextUniqueId(); int frame_tree_node_id = GetNextUniqueId();
int frame_routing_id = GetNextUniqueId(); int frame_routing_id = GetNextUniqueId();
auto frame = CreateNode<FrameNodeImpl>( auto frame_token = blink::LocalFrameToken();
process_.get(), page_.get(), parent, frame_tree_node_id, frame_routing_id, auto frame = CreateNode<FrameNodeImpl>(process_.get(), page, parent,
blink::LocalFrameToken(), browsing_instance_id.id); frame_tree_node_id, frame_routing_id,
frame_token, browsing_instance_id);
frame->OnNavigationCommitted(GURL(url), /*same document*/ true); frame->OnNavigationCommitted(GURL(url), /*same document*/ true);
V8DetailedMemoryExecutionContextData::CreateForTesting(frame.get()) V8DetailedMemoryExecutionContextData::CreateForTesting(frame.get())
->set_v8_bytes_used(memory_usage.bytes); ->set_v8_bytes_used(memory_usage.bytes);
frames_.push_back(std::move(frame)); frames_.push_back(std::move(frame));
return frames_.back().get(); FrameNodeImpl* frame_impl = frames_.back().get();
// Create a V8ContextDescription with attribution data for this frame. (In
// production this is done by PerformanceManager monitoring frame lifetime
// events.)
auto description = mojom::V8ContextDescription::New();
description->token = blink::V8ContextToken();
description->world_type = mojom::V8ContextWorldType::kMain;
description->execution_context_token = frame_token;
mojom::IframeAttributionDataPtr attribution;
if (parent) {
// Frame attribution attributes come from the frame's parent node, so
// V8ContextTracker expects an IframeAttributionData. The attribute values
// may be empty.
attribution = mojom::IframeAttributionData::New();
attribution->id = id_attribute;
attribution->src = src_attribute;
} else {
// V8ContextTracker expects no IframeAttributionData.
DCHECK(!id_attribute);
DCHECK(!src_attribute);
}
DCHECK(frame_impl->process_node());
frame_impl->process_node()->OnV8ContextCreated(std::move(description),
std::move(attribution));
return frame_impl;
} }
void WebMemoryAggregatorTest::MeasureAndVerify( void WebMemoryAggregatorTest::MeasureAndVerify(
...@@ -107,8 +199,7 @@ void WebMemoryAggregatorTest::MeasureAndVerify( ...@@ -107,8 +199,7 @@ void WebMemoryAggregatorTest::MeasureAndVerify(
base::flat_map<std::string, Bytes> actual; base::flat_map<std::string, Bytes> actual;
for (const auto& entry : result->breakdown) { for (const auto& entry : result->breakdown) {
EXPECT_EQ(1u, entry->attribution.size()); EXPECT_EQ(1u, entry->attribution.size());
EXPECT_EQ(mojom::WebMemoryAttribution::Scope::kWindow, EXPECT_EQ(AttributionScope::kWindow, entry->attribution[0]->scope);
entry->attribution[0]->scope);
actual[*entry->attribution[0]->url] = Bytes{entry->bytes}; actual[*entry->attribution[0]->url] = Bytes{entry->bytes};
} }
EXPECT_EQ(expected, actual); EXPECT_EQ(expected, actual);
...@@ -119,10 +210,75 @@ void WebMemoryAggregatorTest::MeasureAndVerify( ...@@ -119,10 +210,75 @@ void WebMemoryAggregatorTest::MeasureAndVerify(
EXPECT_TRUE(measurement_done); EXPECT_TRUE(measurement_done);
} }
TEST_F(WebMemoryAggregatorTest, IncludeSameOriginRelatedFrames) { struct ExpectedMemoryBreakdown {
auto* main = AddFrameNode("http://foo.com/", BrowsingInstance{0}, Bytes{10u}); uint64_t bytes = 0U;
AttributionScope scope = AttributionScope::kWindow;
base::Optional<std::string> url;
base::Optional<std::string> id;
base::Optional<std::string> src;
ExpectedMemoryBreakdown() = default;
ExpectedMemoryBreakdown(
uint64_t expected_bytes,
AttributionScope expected_scope,
base::Optional<std::string> expected_url = base::nullopt,
base::Optional<std::string> expected_id = base::nullopt,
base::Optional<std::string> expected_src = base::nullopt)
: bytes(expected_bytes),
scope(expected_scope),
url(std::move(expected_url)),
id(std::move(expected_id)),
src(std::move(expected_src)) {}
ExpectedMemoryBreakdown(const ExpectedMemoryBreakdown& other) = default;
ExpectedMemoryBreakdown& operator=(const ExpectedMemoryBreakdown& other) =
default;
};
AddFrameNode("http://foo.com/iframe", BrowsingInstance{0}, Bytes{20}, main); mojom::WebMemoryMeasurementPtr CreateExpectedMemoryMeasurement(
const std::vector<ExpectedMemoryBreakdown>& breakdowns) {
auto expected_measurement = mojom::WebMemoryMeasurement::New();
for (const auto& breakdown : breakdowns) {
auto expected_breakdown = mojom::WebMemoryBreakdownEntry::New();
expected_breakdown->bytes = breakdown.bytes;
auto attribution = mojom::WebMemoryAttribution::New();
attribution->scope = breakdown.scope;
attribution->url = breakdown.url;
attribution->id = breakdown.id;
attribution->src = breakdown.src;
expected_breakdown->attribution.push_back(std::move(attribution));
expected_measurement->breakdown.push_back(std::move(expected_breakdown));
}
return expected_measurement;
}
// Abuse Mojo's trace integration to serialize a measurement to sorted JSON for
// string comparison. This gives failure messages that include the full
// measurement in JSON format and is easier than comparing every field of
// nested Mojo messages individually.
std::string MeasurementToJSON(
const mojom::WebMemoryMeasurementPtr& measurement) {
// Sort all arrays.
auto canonical_measurement = measurement->Clone();
for (const auto& breakdown_entry : canonical_measurement->breakdown) {
std::sort(breakdown_entry->attribution.begin(),
breakdown_entry->attribution.end());
}
std::sort(canonical_measurement->breakdown.begin(),
canonical_measurement->breakdown.end());
// Convert to JSON string.
base::trace_event::TracedValueJSON json_value;
canonical_measurement->AsValueInto(&json_value);
return json_value.ToJSON();
}
TEST_F(WebMemoryAggregatorTest, MeasurerIncludesSameOriginRelatedFrames) {
auto* main = AddFrameNode("http://foo.com/", Bytes{10u});
AddFrameNode("http://foo.com/iframe", Bytes{20}, main);
MeasureAndVerify(main, { MeasureAndVerify(main, {
{"http://foo.com/", Bytes{10u}}, {"http://foo.com/", Bytes{10u}},
...@@ -130,18 +286,21 @@ TEST_F(WebMemoryAggregatorTest, IncludeSameOriginRelatedFrames) { ...@@ -130,18 +286,21 @@ TEST_F(WebMemoryAggregatorTest, IncludeSameOriginRelatedFrames) {
}); });
} }
TEST_F(WebMemoryAggregatorTest, SkipCrossOriginFrames) { // TODO(b/1085129): Currently WebMemoryMeasurer only includes the results for a
auto* main = AddFrameNode("http://foo.com", BrowsingInstance{0}, Bytes{10u}); // single process. Once it invokes WebMemoryAggregator, update this test to
// expect cross-origin frames to be included in the aggregation.
TEST_F(WebMemoryAggregatorTest, MeasurerSkipsCrossOriginFrames) {
auto* main = AddFrameNode("http://foo.com", Bytes{10u});
AddFrameNode("http://bar.com/iframe", BrowsingInstance{0}, Bytes{20}, main); AddFrameNode("http://bar.com/iframe", Bytes{20}, main);
MeasureAndVerify(main, {{"http://foo.com/", Bytes{10u}}}); MeasureAndVerify(main, {{"http://foo.com/", Bytes{10u}}});
} }
TEST_F(WebMemoryAggregatorTest, SkipUnrelatedFrames) { TEST_F(WebMemoryAggregatorTest, MeasurerSkipsCrossBrowserContextGroupFrames) {
auto* main = AddFrameNode("http://foo.com", BrowsingInstance{0}, Bytes{10u}); auto* main = AddFrameNode("http://foo.com", Bytes{10u});
AddFrameNode("http://foo.com/unrelated", BrowsingInstance{1}, Bytes{20}); AddCrossBrowsingInstanceFrameNode("http://foo.com/unrelated", Bytes{20});
MeasureAndVerify(main, {{"http://foo.com/", Bytes{10u}}}); MeasureAndVerify(main, {{"http://foo.com/", Bytes{10u}}});
} }
...@@ -235,6 +394,418 @@ TEST_F(WebMemoryAggregatorPMTest, MeasurementInterrupted) { ...@@ -235,6 +394,418 @@ TEST_F(WebMemoryAggregatorPMTest, MeasurementInterrupted) {
task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(5)); task_environment()->FastForwardBy(base::TimeDelta::FromSeconds(5));
} }
TEST_F(WebMemoryAggregatorTest, CreateBreakdownEntry) {
auto measurement = mojom::WebMemoryMeasurement::New();
auto* breakdown_with_no_url =
internal::CreateBreakdownEntry(AttributionScope::kCrossOriginAggregated,
base::nullopt, measurement.get());
auto* breakdown_with_url = internal::CreateBreakdownEntry(
AttributionScope::kWindow, "https://example.com", measurement.get());
auto* breakdown_with_empty_url = internal::CreateBreakdownEntry(
AttributionScope::kWindow, "", measurement.get());
// Ensure breakdowns were added to measurement.
EXPECT_EQ(measurement->breakdown.size(), 3U);
EXPECT_EQ(measurement->breakdown[0].get(), breakdown_with_no_url);
EXPECT_EQ(measurement->breakdown[1].get(), breakdown_with_url);
EXPECT_EQ(measurement->breakdown[2].get(), breakdown_with_empty_url);
// Can't use an initializer list because nullopt_t and
// base::Optional<std::string> are different types.
std::vector<base::Optional<std::string>> attributes;
attributes.push_back(base::nullopt);
attributes.push_back(base::make_optional("example_attr"));
attributes.push_back(base::make_optional(""));
for (const auto& attribute : attributes) {
SCOPED_TRACE(attribute.value_or("nullopt"));
// V8ContextTracker needs a parent frame to store attributes.
FrameNodeImpl* parent_frame =
attribute ? AddFrameNode("https://example.com", Bytes{1}, nullptr)
: nullptr;
FrameNodeImpl* frame = AddFrameNode("https://example.com", Bytes{1},
parent_frame, attribute, attribute);
internal::SetBreakdownAttributionFromFrame(frame, breakdown_with_url);
internal::CopyBreakdownAttribution(breakdown_with_url,
breakdown_with_empty_url);
// All measurements should be created with 0 bytes.
auto expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(0, AttributionScope::kCrossOriginAggregated,
/*expected_url=*/base::nullopt,
/*expected_id=*/base::nullopt,
/*expected_src=*/base::nullopt),
ExpectedMemoryBreakdown(0, AttributionScope::kWindow,
"https://example.com", attribute, attribute),
ExpectedMemoryBreakdown(0, AttributionScope::kWindow,
/*expected_url=*/"", attribute, attribute),
});
EXPECT_EQ(MeasurementToJSON(measurement),
MeasurementToJSON(expected_result));
}
}
TEST_F(WebMemoryAggregatorTest, AggregateSingleFrame) {
// Example 1 from http://wicg.github.io/performance-measure-memory/#examples
FrameNodeImpl* main_frame = AddFrameNode("https://example.com/", Bytes{10});
auto expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(10, AttributionScope::kWindow,
"https://example.com/"),
});
EXPECT_EQ(internal::FindAggregationStartNode(main_frame), main_frame);
WebMemoryAggregator aggregator(main_frame);
auto result = aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(result), MeasurementToJSON(expected_result));
}
TEST_F(WebMemoryAggregatorTest, AggregateSingleSiteMultiFrame) {
// Example 2 from http://wicg.github.io/performance-measure-memory/#examples
FrameNodeImpl* main_frame = AddFrameNode("https://example.com/", Bytes{10});
FrameNodeImpl* child_frame =
AddFrameNode("https://example.com/iframe.html", Bytes{5}, main_frame,
"example-id", "redirect.html?target=iframe.html");
EXPECT_EQ(internal::FindAggregationStartNode(main_frame), main_frame);
WebMemoryAggregator aggregator(main_frame);
// Test the relationships of each node in the graph.
EXPECT_EQ(aggregator.FindNodeAggregationType(main_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(aggregator.FindNodeAggregationType(child_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
child_frame, aggregator.requesting_origin()),
main_frame);
auto expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(10, AttributionScope::kWindow,
"https://example.com/"),
ExpectedMemoryBreakdown(5, AttributionScope::kWindow,
"https://example.com/iframe.html", "example-id",
"redirect.html?target=iframe.html"),
});
auto result = aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(result), MeasurementToJSON(expected_result));
}
TEST_F(WebMemoryAggregatorTest, AggregateCrossOrigin) {
// Example 5 from http://wicg.github.io/performance-measure-memory/#examples
//
// example.com (10 bytes)
// |
// *--foo.com/iframe1 (5 bytes)
// |
// *--foo.com/iframe2 (2 bytes)
// |
// *--bar.com/iframe2 (3 bytes)
// |
// *--foo.com/worker.js (4 bytes)
FrameNodeImpl* main_frame = AddFrameNode("https://example.com/", Bytes{10});
FrameNodeImpl* child_frame =
AddFrameNode("https://foo.com/iframe1", Bytes{5}, main_frame,
"example-id", "https://foo.com/iframe1");
FrameNodeImpl* grandchild1 =
AddFrameNode("https://foo.com/iframe2", Bytes{2}, child_frame,
"example-id2", "https://foo.com/iframe2");
FrameNodeImpl* grandchild2 =
AddFrameNode("https://bar.com/iframe2", Bytes{3}, child_frame,
"example-id3", "https://bar.com/iframe2");
// TODO(crbug.com/1085129): In the spec this is a worker, but they're not
// supported yet.
FrameNodeImpl* grandchild3 =
AddFrameNode("https://foo.com/worker.js", Bytes{4}, child_frame);
EXPECT_EQ(internal::FindAggregationStartNode(main_frame), main_frame);
WebMemoryAggregator aggregator(main_frame);
// Test the relationships of each node in the graph.
EXPECT_EQ(aggregator.FindNodeAggregationType(main_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(aggregator.FindNodeAggregationType(child_frame),
NodeAggregationType::kCrossOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
child_frame, aggregator.requesting_origin()),
main_frame);
EXPECT_EQ(aggregator.FindNodeAggregationType(grandchild1),
NodeAggregationType::kCrossOriginAggregated);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
grandchild1, aggregator.requesting_origin()),
nullptr);
EXPECT_EQ(aggregator.FindNodeAggregationType(grandchild2),
NodeAggregationType::kCrossOriginAggregated);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
grandchild2, aggregator.requesting_origin()),
nullptr);
EXPECT_EQ(aggregator.FindNodeAggregationType(grandchild3),
NodeAggregationType::kCrossOriginAggregated);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
grandchild3, aggregator.requesting_origin()),
nullptr);
auto expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(10, AttributionScope::kWindow,
"https://example.com/"),
ExpectedMemoryBreakdown(14, AttributionScope::kCrossOriginAggregated,
base::nullopt, "example-id",
"https://foo.com/iframe1"),
});
auto result = aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(result), MeasurementToJSON(expected_result));
}
TEST_F(WebMemoryAggregatorTest, AggregateNestedCrossOrigin) {
// Based on example 6 from
// http://wicg.github.io/performance-measure-memory/#examples with some
// further nested frames added to test all combinations of same-origin &
// cross-origin children & parents.
//
// example.com (10 bytes)
// |
// *--foo.com/iframe1 (5 bytes) <-- opaque to requesting node
// |
// *--bar.com/iframe1 (4 bytes) <-- invisible to requesting node
// |
// *--example.com/iframe1 (3 bytes)
// |
// *--foo.com/iframe2 (2 bytes) <-- opaque to requesting node
// | |
// | *--example.com/iframe2 (1 byte)
// |
// *--example.com/iframe3 (6 bytes)
FrameNodeImpl* main_frame = AddFrameNode("https://example.com/", Bytes{10});
FrameNodeImpl* subframe =
AddFrameNode("https://foo.com/iframe1", Bytes{5}, main_frame,
"example-id", "https://foo.com/iframe1");
FrameNodeImpl* subframe2 =
AddFrameNode("https://bar.com/iframe1", Bytes{4}, subframe, "example-id2",
"https://bar.com/iframe1");
FrameNodeImpl* subframe3 =
AddFrameNode("https://example.com/iframe1", Bytes{3}, subframe2,
"example-id3", "https://example.com/iframe1");
FrameNodeImpl* subframe4 =
AddFrameNode("https://foo.com/iframe2", Bytes{2}, subframe3,
"example-id4", "https://foo.com/iframe2");
FrameNodeImpl* subframe5 =
AddFrameNode("https://example.com/iframe2", Bytes{1}, subframe4,
"example-id5", "https://example.com/iframe2");
FrameNodeImpl* subframe6 =
AddFrameNode("https://example.com/iframe3", Bytes{6}, subframe3,
"example-id6", "https://example.com/iframe3");
// A frame with 0 bytes of memory use (eg. a frame that's added to the frame
// tree during the measurement) should not appear in the result.
FrameNodeImpl* empty_frame =
AddFrameNode("https://example.com/empty_frame", Bytes{0}, subframe3);
EXPECT_EQ(internal::FindAggregationStartNode(main_frame), main_frame);
WebMemoryAggregator aggregator(main_frame);
// Test the relationships of each node in the graph.
EXPECT_EQ(aggregator.FindNodeAggregationType(main_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(aggregator.FindNodeAggregationType(subframe),
NodeAggregationType::kCrossOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
subframe, aggregator.requesting_origin()),
main_frame);
EXPECT_EQ(aggregator.FindNodeAggregationType(subframe2),
NodeAggregationType::kCrossOriginAggregated);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
subframe2, aggregator.requesting_origin()),
nullptr);
EXPECT_EQ(aggregator.FindNodeAggregationType(subframe3),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
subframe3, aggregator.requesting_origin()),
nullptr);
EXPECT_EQ(aggregator.FindNodeAggregationType(subframe4),
NodeAggregationType::kCrossOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
subframe4, aggregator.requesting_origin()),
subframe3);
EXPECT_EQ(aggregator.FindNodeAggregationType(subframe5),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
subframe5, aggregator.requesting_origin()),
nullptr);
EXPECT_EQ(aggregator.FindNodeAggregationType(subframe6),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
subframe6, aggregator.requesting_origin()),
subframe3);
EXPECT_EQ(aggregator.FindNodeAggregationType(empty_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
empty_frame, aggregator.requesting_origin()),
subframe3);
auto expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(10, AttributionScope::kWindow,
"https://example.com/"),
ExpectedMemoryBreakdown(9, AttributionScope::kCrossOriginAggregated,
base::nullopt, "example-id",
"https://foo.com/iframe1"),
ExpectedMemoryBreakdown(3, AttributionScope::kWindow,
"https://example.com/iframe1", "example-id",
"https://foo.com/iframe1"),
ExpectedMemoryBreakdown(2, AttributionScope::kCrossOriginAggregated,
base::nullopt, "example-id4",
"https://foo.com/iframe2"),
ExpectedMemoryBreakdown(1, AttributionScope::kWindow,
"https://example.com/iframe2", "example-id4",
"https://foo.com/iframe2"),
ExpectedMemoryBreakdown(6, AttributionScope::kWindow,
"https://example.com/iframe3", "example-id6",
"https://example.com/iframe3"),
});
auto result = aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(result), MeasurementToJSON(expected_result));
}
TEST_F(WebMemoryAggregatorTest, FindAggregationStartNode) {
FrameNodeImpl* main_frame = AddFrameNode("https://example.com/", Bytes{10});
FrameNodeImpl* cross_site_child = AddFrameNode(
"https://foo.com/iframe.html", Bytes{5}, main_frame, "example-id", "");
FrameNodeImpl* same_site_child =
AddFrameNode("https://foo.com/iframe2.html", Bytes{4}, cross_site_child,
"example-id2", "");
// FindAggregationStartNode should return the parent foo.com frame for either
// foo.com child. It should not return the main frame since it's cross-site
// from the requesting frames.
EXPECT_EQ(internal::FindAggregationStartNode(cross_site_child),
cross_site_child);
EXPECT_EQ(internal::FindAggregationStartNode(same_site_child),
cross_site_child);
// When aggregation starts at |cross_site_child| it should not include any
// memory from the main frame.
WebMemoryAggregator aggregator(cross_site_child);
auto expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(5, AttributionScope::kWindow,
"https://foo.com/iframe.html"),
ExpectedMemoryBreakdown(4, AttributionScope::kWindow,
"https://foo.com/iframe2.html", "example-id2",
""),
});
auto result = aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(result), MeasurementToJSON(expected_result));
// When the main frame requests a measurement of the same tree it should
// aggregate the children, which are cross-site from it.
EXPECT_EQ(internal::FindAggregationStartNode(main_frame), main_frame);
auto main_frame_expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(10, AttributionScope::kWindow,
"https://example.com/"),
ExpectedMemoryBreakdown(9, AttributionScope::kCrossOriginAggregated,
base::nullopt, "example-id", ""),
});
WebMemoryAggregator main_frame_aggregator(main_frame);
auto main_frame_result = main_frame_aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(main_frame_result),
MeasurementToJSON(main_frame_expected_result));
}
TEST_F(WebMemoryAggregatorTest, AggregateWindowOpener) {
FrameNodeImpl* main_frame = AddFrameNode("https://example.com/", Bytes{10});
FrameNodeImpl* child_frame = AddFrameNode("https://example.com/iframe.html",
Bytes{5}, main_frame, "example-id");
FrameNodeImpl* opened_frame = AddFrameNodeFromOpener(
"https://example.com/window/", Bytes{4}, main_frame);
FrameNodeImpl* child_of_opened_frame =
AddFrameNode("https://example.com/window-iframe.html", Bytes{3},
opened_frame, "example-id2");
FrameNodeImpl* cross_site_child =
AddFrameNode("https://cross-site-example.com/window-iframe.html",
Bytes{2}, opened_frame, "example-id3");
// COOP+COEP forces cross-site windows to open in their own BrowsingInstance.
FrameNodeImpl* cross_site_popup = AddCrossBrowsingInstanceFrameNodeFromOpener(
"https://cross-site-example.com/", Bytes{2}, main_frame);
// FindAggregationStartNode whould return |main_frame| from any of the
// same-site frames.
for (auto* frame :
{main_frame, child_frame, opened_frame, child_of_opened_frame}) {
EXPECT_EQ(internal::FindAggregationStartNode(frame), main_frame)
<< frame->url();
}
WebMemoryAggregator aggregator(main_frame);
// Test the relationships of each node in the graph.
EXPECT_EQ(aggregator.FindNodeAggregationType(main_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(aggregator.FindNodeAggregationType(child_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
child_frame, aggregator.requesting_origin()),
main_frame);
EXPECT_EQ(aggregator.FindNodeAggregationType(opened_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
opened_frame, aggregator.requesting_origin()),
main_frame);
EXPECT_EQ(aggregator.FindNodeAggregationType(child_of_opened_frame),
NodeAggregationType::kSameOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
child_of_opened_frame, aggregator.requesting_origin()),
opened_frame);
EXPECT_EQ(aggregator.FindNodeAggregationType(cross_site_child),
NodeAggregationType::kCrossOriginAggregationPoint);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
cross_site_child, aggregator.requesting_origin()),
opened_frame);
EXPECT_EQ(aggregator.FindNodeAggregationType(cross_site_popup),
NodeAggregationType::kInvisible);
EXPECT_EQ(internal::GetSameOriginParentOrOpener(
cross_site_popup, aggregator.requesting_origin()),
main_frame);
auto expected_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(10, AttributionScope::kWindow,
"https://example.com/"),
ExpectedMemoryBreakdown(5, AttributionScope::kWindow,
"https://example.com/iframe.html", "example-id"),
ExpectedMemoryBreakdown(4, AttributionScope::kWindow,
"https://example.com/window/"),
ExpectedMemoryBreakdown(3, AttributionScope::kWindow,
"https://example.com/window-iframe.html",
"example-id2"),
ExpectedMemoryBreakdown(2, AttributionScope::kCrossOriginAggregated,
base::nullopt, "example-id3"),
});
auto result = aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(result), MeasurementToJSON(expected_result));
// The two cross-site frames should only be able to see themselves (and their
// own children, if they had any). They have the same |bytes| so their
// expectations only vary by url.
for (auto* frame : {cross_site_child, cross_site_popup}) {
const std::string url = frame->url().spec();
SCOPED_TRACE(url);
const FrameNode* start_node = internal::FindAggregationStartNode(frame);
EXPECT_EQ(start_node, frame);
WebMemoryAggregator aggregator(start_node);
// Only check the NodeAggregationType of the single node that's iterated
// over. Parents of the start node have an undefined aggregation type.
EXPECT_EQ(aggregator.FindNodeAggregationType(start_node),
NodeAggregationType::kSameOriginAggregationPoint);
auto expected_cross_site_result = CreateExpectedMemoryMeasurement({
ExpectedMemoryBreakdown(2, AttributionScope::kWindow, url,
base::nullopt, base::nullopt),
});
auto cross_site_result = aggregator.AggregateMeasureMemoryResult();
EXPECT_EQ(MeasurementToJSON(cross_site_result),
MeasurementToJSON(expected_cross_site_result));
}
}
} // namespace v8_memory } // namespace v8_memory
} // namespace performance_manager } // namespace performance_manager
...@@ -114,9 +114,12 @@ namespace { ...@@ -114,9 +114,12 @@ namespace {
// These functions convert WebMemory* mojo structs to IDL and JS values. // These functions convert WebMemory* mojo structs to IDL and JS values.
WTF::String ConvertScope(WebMemoryAttribution::Scope scope) { WTF::String ConvertScope(WebMemoryAttribution::Scope scope) {
using Scope = WebMemoryAttribution::Scope;
switch (scope) { switch (scope) {
case WebMemoryAttribution::Scope::kWindow: case Scope::kWindow:
return "Window"; return "Window";
case Scope::kCrossOriginAggregated:
return "cross-origin-aggregated";
} }
} }
......
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