зеркало из https://github.com/mozilla/gecko-dev.git
202 строки
9.3 KiB
Markdown
202 строки
9.3 KiB
Markdown
|
# Memory Tool Architecture
|
||
|
|
||
|
The memory tool is built of three main elements:
|
||
|
|
||
|
1. `JS::ubi::Node` provides an interface to either the live heap graph, or a
|
||
|
serialized, offline snapshot of some heap graph from a previous moment in
|
||
|
time. Our various heap analyses (census, dominator trees, shortest paths,
|
||
|
etc) run on top of `JS::ubi::Node` graphs.
|
||
|
|
||
|
2. The `HeapAnalysesWorker` runs in a worker thread, performing analyses on
|
||
|
snapshots and translating the results into something the frontend can render
|
||
|
simply and quickly.
|
||
|
|
||
|
3. Finally, the last element is the frontend that renders data received from the
|
||
|
`HeapAnalysesClient` to the DOM and translates user input into requests for
|
||
|
new data with the `HeapAnalysesClient`.
|
||
|
|
||
|
Unlike other tools (such as the JavaScript debugger), the memory tool makes very
|
||
|
little use of the Remote Debugger Server and the actors that reside in it. Use
|
||
|
of the [`MemoryActor`](devtools/server/actors/memory.js) is limited to toggling
|
||
|
allocation stack recording on and off, and transferring heap snapshots from the
|
||
|
debuggee (which is on the server) to the `HeapAnalysesWorker` (which is on the
|
||
|
client). A nice benefit that naturally emerges, is that supporting "legacy"
|
||
|
servers (eg, using Firefox Developer Edition as a client to remote debug a
|
||
|
release Firefox for Android server) is a no-op. As we add new analyses, we can
|
||
|
run them on snapshots taken on old servers no problem. The only requirement is
|
||
|
that changes to the snapshot format itself remain backwards compatible.
|
||
|
|
||
|
## `JS::ubi::Node`
|
||
|
|
||
|
`JS::ubi::Node` itself is very well documented in the `js/public/UbiNode.h`
|
||
|
header. I suggest you at least skim that documentation before continuing.
|
||
|
|
||
|
A "heap snapshot" is a representation of the heap graph at some particular past
|
||
|
instance in time.
|
||
|
|
||
|
A "heap analysis" is an algorithm that runs on a `JS::ubi::Node` heap
|
||
|
graph. That's it. Generally, analyses can run on either the live heap graph or a
|
||
|
deserialized snapshot. Example analyses include "census", which aggregates and
|
||
|
counts nodes into various user-specified buckets; "dominator trees", which
|
||
|
compute the "dominates" relation and retained size for all nodes in the heap
|
||
|
graph; and "shortest paths" which finds the shortest paths from the GC roots to
|
||
|
some subset of nodes.
|
||
|
|
||
|
### Saving Heap Snapshots
|
||
|
|
||
|
Saving a heap snapshot has a few requirements:
|
||
|
|
||
|
1. The binary format must remain backwards compatible and future extensible.
|
||
|
|
||
|
2. The live heap graph must not mutate while we are in the process of
|
||
|
serializing it.
|
||
|
|
||
|
3. The act of saving a heap snapshot should impose as little memory overhead as
|
||
|
possible. If we are taking a snapshot to debug frequent out-of-memory errors,
|
||
|
we don't want to trigger an OOM ourselves!
|
||
|
|
||
|
To solve (1), we use the protobuf message format. The message definitions
|
||
|
themselves are in `devtools/shared/heapsnapshot/CoreDump.proto`. We always use
|
||
|
`optional` fields so we can change our mind about what fields are required
|
||
|
sometime in the future. Deserialization checks the semantic integrity of
|
||
|
deserialized protobuf messages.
|
||
|
|
||
|
For (2), we rely on SpiderMonkey's GC rooting hazard static analysis and the
|
||
|
`AutoCheckCannotGC` dynamic analysis to ensure that neither JS nor GC runs and
|
||
|
modifies objects or moves them from one address in memory to another. There is
|
||
|
no equivalent suppression and static analysis technique for the
|
||
|
[cycle collector](https://developer.mozilla.org/en/docs/Interfacing_with_the_XPCOM_cycle_collector),
|
||
|
so care must be taken not to invoke methods that could start cycle collection or
|
||
|
mutate the heap graph from the cycle collector's perspective. At the time of
|
||
|
writing, we don't yet support saving the cycle collector's portion of the heap
|
||
|
graph in snapshots, but that work is deemed Very Important and Very High
|
||
|
Priority.
|
||
|
|
||
|
Finally, (3) imposes upon us that we do not build the serialized heap snapshot
|
||
|
binary blob in memory, but instead stream it out to disk while generating it.
|
||
|
|
||
|
Once all of that is accounted for, saving snapshots becomes pretty straight
|
||
|
forward. We traverse the live heap graph with `JS::ubi::Node` and
|
||
|
`JS::ubi::BreadthFirst`, create a protobuf message for each node and each node's
|
||
|
edges, and write these messages to disk before continuing the traversal to the
|
||
|
next node.
|
||
|
|
||
|
This functionality is exposed to chrome JavaScript as the
|
||
|
`[ThreadSafe]ChromeUtils.saveHeapSnapshot` function. See
|
||
|
`dom/webidl/ThreadSafeChromeUtils.webidl` for API documentation.
|
||
|
|
||
|
### Reading Heap Snapshots
|
||
|
|
||
|
Reading heap snapshots has less restrictions than saving heap snapshots. The
|
||
|
protobuf messages that make up the core dump are deserialized one by one, stored
|
||
|
as a set of `DeserializedNode`s and a set of `DeserializedEdge`s, and the result
|
||
|
is a `HeapSnapshot` instance.
|
||
|
|
||
|
The `DeserializedNode` and `DeserializedEdge` classes implement the
|
||
|
`JS::ubi::Node` interface. Analyses running on offline heap snapshots rather
|
||
|
than the live heap graph operate on these classes (unknowingly, of course).
|
||
|
|
||
|
For more details, see the
|
||
|
[`mozilla::devtools::HeapSnapshot`](devtools/shared/heapsnapshot/HeapSnapshot.cpp)
|
||
|
and
|
||
|
[`mozilla::devtools::Deserialized{Node,Edge}`](devtools/shared/heapsnapshot/DeserializedNode.h)
|
||
|
classes.
|
||
|
|
||
|
### Heap Analyses
|
||
|
|
||
|
Heap analyses operate on `JS::ubi::Node` graphs without knowledge of whether
|
||
|
that graph is backed by the live heap graph or an offline heap snapshot. They
|
||
|
must make sure never to allocate GC things or modify the live heap graph.
|
||
|
|
||
|
In general, analyses are implemented in their own `js/public/UbiFooBar.h` header
|
||
|
(eg `js/public/UbiCensus.h`), and are exposed to chrome JavaScript code via a
|
||
|
method on the [`HeapSnapshot`](dom/webidl/HeapSnapshot.webidl) webidl
|
||
|
interface.
|
||
|
|
||
|
For each analysis we expose to chrome JavaScript on the `HeapSnapshot` webidl
|
||
|
interface, there is a small amount of glue code in Gecko. The
|
||
|
[`mozilla::devtools::HeapSnapshot`](devtools/shared/heapsnapshot/HeapSnapshot.h)
|
||
|
C++ class implements the webidl interface. The analyses methods (eg
|
||
|
`ComputeDominatorTree`) take the deserialized nodes and edges from the heap
|
||
|
snapshot, create `JS::ubi::Node`s from them, call the analyses from
|
||
|
`js/public/Ubi*.h`, and wrap the results in something that can be represented in
|
||
|
JavaScript.
|
||
|
|
||
|
For API documentation on running specific analyses, see the
|
||
|
[`HeapSnapshot`](dom/webidl/HeapSnapshot.webidl) webidl interface.
|
||
|
|
||
|
### Testing `JS::ubi::Node`, Snapshots, and Analyses
|
||
|
|
||
|
The majority of the tests reside within `devtools/shared/heapsnapshot/tests/**`.
|
||
|
For reading and saving heap snapshots, most tests are gtests. The gtests can be
|
||
|
run with the `mach gtest DevTools.*` command. The rest are integration sanity
|
||
|
tests to make sure we can read and save snapshots in various environments, such
|
||
|
as xpcshell or workers. These can be run with the usual `mach test $PATH`
|
||
|
commands.
|
||
|
|
||
|
There are also `JS::ubi::Node` related unit tests in
|
||
|
`js/src/jit-test/tests/heap-analysis/*`, `js/src/jit-test/tests/debug/Memory-*`,
|
||
|
and `js/src/jsapi-tests/testUbiNode.cpp`. See
|
||
|
https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Running_Automated_JavaScript_Tests#Running_jit-tests
|
||
|
for running the JIT tests.
|
||
|
|
||
|
## `HeapAnalysesWorker`
|
||
|
|
||
|
The `HeapAnalysesWorker` orchestrates running specific analyses on snapshots and
|
||
|
transforming the results into something that can simply and quickly be rendered
|
||
|
by the frontend. The analyses can take some time to run (sometimes on the order
|
||
|
of seconds), so doing them in a worker thread allows the interface to stay
|
||
|
responsive. The `HeapAnalysisClient` provides the main thread's interface to the
|
||
|
worker.
|
||
|
|
||
|
The `HeapAnalysesWorker` doesn't actually do much itself; mostly just shuffling
|
||
|
data and transforming it from one representation to another or calling utility
|
||
|
functions that do those things. Most of these are implemented as traversals of
|
||
|
the resulting census or dominator trees.
|
||
|
|
||
|
See the
|
||
|
`devtools/shared/heapsnapshot/{CensusUtils,CensusTreeNode,DominatorTreeNode}.js`
|
||
|
files for details on the various data transformations and shuffling that the
|
||
|
`HeapAnalysesWorker` delegates to.
|
||
|
|
||
|
### Testing the `HeapAnalysesWorker` and `HeapAnalysesClient`
|
||
|
|
||
|
Tests for the `HeapAnalysesWorker` and `HeapAnalysesClient` reside in
|
||
|
`devtools/shared/heapsnapshot/tests/**` and can be run with the usual `mach test
|
||
|
$PATH` command.
|
||
|
|
||
|
## Frontend
|
||
|
|
||
|
The frontend of the memory tool is built with React and Redux.
|
||
|
|
||
|
[React has thorough documentation.](https://facebook.github.io/react/)
|
||
|
|
||
|
[Redux has thorough documentation.](http://rackt.org/redux/index.html)
|
||
|
|
||
|
We have React components in `devtools/client/memory/components/*`.
|
||
|
|
||
|
We have Redux reducers in `devtools/client/memory/reducers/*`.
|
||
|
|
||
|
We have Redux actions and action-creating tasks in
|
||
|
`devtools/client/memory/actions/*`.
|
||
|
|
||
|
React components should be pure functions from their props to the rendered
|
||
|
(virtual) DOM. Redux reducers should also be observably pure.
|
||
|
|
||
|
Impurity within the frontend is confined to the tasks that are creating and
|
||
|
dispatching actions. All communication with the outside world (such as the
|
||
|
`HeapAnalysesWorker`, the Remote Debugger Server, or the file system) is
|
||
|
restricted to within these tasks.
|
||
|
|
||
|
### Testing the Frontend
|
||
|
|
||
|
Unit tests for React components are in `devtools/client/memory/test/chrome/*`.
|
||
|
|
||
|
Unit tests for actions, reducers, and state changes are in
|
||
|
`devtools/client/memory/test/unit/*`.
|
||
|
|
||
|
Holistic integration tests for the frontend and the whole memory tool are in
|
||
|
`devtools/client/memory/test/browser/*`.
|
||
|
|
||
|
All tests can be run with the usual `mach test $PATH` command.
|