UNPKG

22.7 kBMarkdownView Raw
1# Architecture of DevTools
2
3This document explains the high-level architecture as well as any considerations that were made along the way.
4The document is evolving and will be updated whenever architectural changes are being made.
5
6## Guiding principles
7
8Throughout this document, references are included to relevant [Web Platform Design Principles], whenever they are applicable for that specific section.
9It is recommended to be familiar with the Web Platform Design Principles prior to reading to this document, but it is not required.
10There are additional DevTools-specific guiding principles that are listed in this section.
11
12### Load only what is necessary
13
14DevTools is a large web application.
15It contains dozens of features, most of them are distinct.
16As such, loading all features up front is infeasible and can lead to large startup times of DevTools.
17The DevTools architecture should encourage granular implementations of features, lazy loading the minimal amount of code to make features work.
18
19See also:
20* [Put user needs first]
21
22### Prefer web platform features whenever possible
23
24DevTools ships as part of Chromium-based browsers and therefore is long-living.
25Code that is shipped today can live on for years, even decades.
26Therefore, web best practices constantly evolve during the lifespan of DevTools.
27To avoid frequent rewrites of features, each feature should be implemented with longevity in mind.
28Web platform features are standardized and designed to be supported ad infinitum.
29Whenever possible, prefer the usage of web platform features over custom solutions, as custom solutions require constant maintenance and are more likely to become out-of-date.
30
31See also:
32* [Prefer simple solutions]
33* [Put user needs first]
34
35### Design with continuous deployment in mind
36
37DevTools ships every single day in Canary builds of Chromium-based browsers.
38It is therefore risky to halt development during a migration (even for a couple of weeks), as DevTools can cause Canary builds to break and effect not just end-users, but also engineers working on the web platform itself.
39The symbiosis of the web platform and DevTools means that DevTools itself must be kept up-to-date, to support a continuously evolving platform.
40
41Migrations should therefore be gradual and allow for continuous deployment of DevTools in Canary builds.
42The migrations will thus have to take into account not just the desired end solution, but also the limitations of today's implementation.
43In the end, it is not possible to predict when migrations are completed, which means that the codebase can be under migration for a significant amount of time.
44Ensure that migrations do not (strongly) negatively impact feature development and evolution of the wider web platform and can be completed in a timely fashion.
45
46See also:
47* [Put user needs first]
48* [Prefer web platform features whenever possible]
49
50### Use flexible third_party libraries whenever necessary
51
52Not all requirements of DevTools can be fulfilled by web platform features alone.
53There will be situations in which third_party libraries (ideally closely built on top of web platform features) are the appropriate solution.
54Every third_party library introduced in DevTools adds risk to the longterm maintenance of the overall product.
55Therefore, each third_party library that DevTools uses should be flexible: a library should be (relatively) easy to be removed from the product.
56
57In practice, this means that any new third_party library must allow for gradual introduction in the codebase, but (if required) also gradually removal.
58Since third_party libraries can become unmaintained, gradual removal allows continued development of DevTools features, while the impact of the deprecation is dealt with.
59If a third_party library is difficult to remove and has a broad impact on the overall codebase, it could cause a halt of development of DevTools features.
60Since the web platform is continuously evolving and DevTools is a part of the platform focused on web developers, halting feature development can have a negative impact on the wider web platform.
61
62Concretely, the introduction of a framework that takes control of the lifecycle of (parts of) DevTools is practically impossible.
63Such frameworks require difficult-to-execute migrations and typically don't allow for gradual removals.
64Moreover, decisions made by maintainers of third_party frameworks could cause significant maintenance churn for DevTools maintainers.
65
66Note that in this section, the definition of "framework" can differ based on point-of-views of stakeholders and could apply more broadly than initially expected.
67Make sure to evaluate third_party packages based on impact on the DevTools codebase, which could be larger than third_party maintainers might have intended.
68In other words: even if a third_party package is advertised as a library, it could still be considered as a framework from the perspective of DevTools maintainers.
69
70See also:
71* [Load only what is necessary]
72* [Design with continuous deployment in mind]
73
74### Limit implementation possibilities while providing maximum flexibility
75
76Typically, there are multiples ways to implement application features on the web.
77A direct result of the flexibility of the web is the proliferation of different solutions to the same problem.
78A negative consequence of the flexibility is the wide variety of solutions and corresponding maintenance cost in the longterm future.
79The DevTools architecture should limit the amount of possible solutions to various problems, yet providing maximum flexibility to engineers implementing DevTools features.
80
81Sadly, that is easier said than done.
82Even when taking this principle into account when working on DevTools' architecture, it can be relatively easy to discover "architectural regressions" years later.
83On the flipside, it can be appealing to be overly restrictive, to avoid such "architectural regressions".
84However, "unnecessary" (this qualification can be subjective and differ based on point-of-view) restrictions can have a strong negative impact on development of DevTools features and therefore can cause more problems on its own.
85
86Balancing the architectural requirements to ensure a stable and fast-loading DevTools versus the needs of implementing new DevTools features is a continuously evolving process.
87To ensure a healthy balance, a periodic evaluation can be useful to address potential architectural technical debt.
88
89See also:
90* [Prefer simple solutions]
91* [Load only what is necessary]
92
93# Build system
94
95Since DevTools is a complex application that integrates with Chromium, it is built on top of the Chromium build system [GN] built on top of [Ninja].
96The build system ensures that all relevant files are correctly defined for consumption by Chromium.
97It also integrates with (third_party) tooling such as a type checker ([TypeScript]) and upstream Chromium packages ([Chrome DevTools Protocol]).
98
99DevTools-specific GN templates are defined in [scripts/build/ninja/](scripts/build/ninja/).
100Chromium consumes the output of the DevTools GN target [:generate_devtools_grd defined in BUILD.gn](BUILD.gn), which generates a GRD bundle using [GRIT].
101Detailed descriptions about each template are included in [scripts/build/ninja/README.md](scripts/build/ninja/README.md).
102
103# Startup process overview
104
105DevTools startup process is based on a core-feature model.
106The application builds on top of a core of common functionality that is shared between features.
107The core is responsible for communicating with the Chromium backend and building a foundation of UI functionality to facilitate the definition of higher-level panels containing features.
108Each feature is declared upfront and lazily loaded whenever the user interacts with the feature [[Load only what is necessary]].
109
110Loading of core functionality and features is built on top of [JavaScript modules].
111Core functionality is loaded via static imports, while implementations of features is lazily loaded using [dynamic imports].
112Features themselves use static imports for loading core and feature-specific functionality.
113
114![A high-level overview of how feature implementations are lazily loaded in the DevTools startup process](./docs/images/architecture-lazy-loading-features.png)
115
116Enforcement of the rules regarding loading is implemented using the [ESLint] rule defined in [scripts/eslint_rules/lib/es_modules_import.js](scripts/eslint_rules/lib/es_modules_import.js).
117
118## DevTools application entrypoints
119
120There are multiple variants of the DevTools application.
121The main DevTools application is [front_end/devtools_app.js](front_end/devtools_app/devtools_app.js), which developers can open via F12 or "Right click -> Inspect element" in their Chromium browser.
122Other application entrypoints are used when debugging via Node ([front_end/node_app.js](front_end/node_app/node_app.js)), a slimmed down V8-specific Node debugging context ([front_end/js_app.js](front_end/js_app/js_app.js)), a (service) worker context ([front_end/worker_app.js](front_end/worker_app/worker_app.js)), a standalone browser window ([front_end/toolbox.ts](front_end/toolbox/toolbox.ts)), remote debugging of (Android) devices with `chrome://inspect/#devices` ([front_end/inspector.js](front_end/inspector/inspector.js)), standalone Node debugger [ndb] ([front_end/ndb_app.js](front_end/ndb_app/ndb_app.js)) and a legacy entrypoint for Chromium layout tests ([front_end/integration_test_runner/integration_test_runner.js](front_end/integration_test_runner.js)).
123
124Each application entrypoint has a corresponding `.html` file with the same name, that can be loaded by the Chromium DevTools backend.
125The JavaScript files import the relevant "meta" files containing the declarations of DevTools features.
126
127## Upfront declaration of DevTools features
128
129The upfront declaration of DevTools features is also known as "extensions".
130Extensions add functionality to the DevTools application using declarative registration calls.
131
132There are multiple types of extensions, including how DevTools handles its own internal business logic or to declare user-facing features with localized strings.
133There are 4 main types of extensions:
134
135* [UI.ActionRegistration.Action](front_end/ui/ActionRegistration.ts)
136* [UI.View.View](front_end/ui/View.js)
137* [Common.Settings.Setting](front_end/common/Settings.js)
138* General type lookups.
139
140Each specific extension is documented in the README of their respective folder.
141
142The registration of a particular extension implemented in `module` must always be declared in a `<module>-meta.ts` in the same folder.
143The meta files should be included by all relevant DevTools application entrypoints.
144If you want to make functionality available in all DevTools application entrypoints, you can import it in [shell.js](front_end/shell.js).
145
146For example, the meta declaration file for [front_end/network/](front_end/network/) is called [front_end/network/network-meta.ts](front_end/network/network-meta.ts) and defined in [front_end/network/BUILD.gn](front_end/network/BUILD.gn):
147
148```python
149devtools_entrypoint("meta") {
150 entrypoint = "network-meta.ts"
151 deps = [
152 ":bundle",
153 "../root:bundle",
154 "../ui:bundle",
155 ]
156}
157```
158
159Below is an example implementation of a `<module>-meta.ts` (using [front_end/network/network-meta.ts](front_end/network/network-meta.ts) as running example).
160For information about the localization system, please see the documentation in [docs/localization/](docs/localization/).
161
162```ts
163// Any static imports on core modules
164import * as Common from '../common/common.js';
165import * as UI from '../ui/ui.js';
166
167// A type-import that is removed during compilation by TypeScript. Therefore,
168// it is not a static import on runtime and adheres to the lazy loading
169// rules defined for the startup process.
170import type * as Network from './network.js';
171
172import * as i18n from '../i18n/i18n.js';
173const UIStrings = {
174 // UIStrings definitions here
175};
176const str_ = i18n.i18n.registerUIStrings('network/network-meta.ts', UIStrings);
177// Since meta files are loaded synchronously during startup, the localization system
178// has not finished loading yet and we need to lazily compute localized strings.
179const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
180// The result of the dynamically loaded module. Will be undefined until the user has
181// started using a feature defined in this module.
182let loadedNetworkModule: (typeof Network|undefined);
183
184// Lazily load the functionality for the network panel. This function will only dynamically
185// import the module once.
186async function loadNetworkModule(): Promise<typeof Network> {
187 if (!loadedNetworkModule) {
188 // Dynamic import to load `front_end/network/network.ts`, which contains the
189 // actual implementation of the Network panel.
190 loadedNetworkModule = await import('./network.js');
191 }
192 return loadedNetworkModule;
193}
194
195// Retrieve any context types from the lazily loaded module, but only if the module has
196// already been loaded at least once. The context types are used to determine the availibility
197// of certain DevTools extensions, for example certain feature-specific command menu entries
198// like the debugger actions when debugging source code. For more information, see the usage
199// of this function down below.
200function maybeRetrieveContextTypes<T = unknown>(getClassCallBack: (loadedNetworkModule: typeof Network) => T[]): T[] {
201 if (loadedNetworkModule === undefined) {
202 return [];
203 }
204 return getClassCallBack(loadedNetworkModule);
205}
206
207// A top-level panel. This is the main extension for a high-level feature. The panel is
208// loaded whenever the user clicks on the panel tab or loads it via the "More Tools" menu.
209// For more information about view extensions, please see the documentation in
210// `front_end/ui/View.js`.
211UI.ViewManager.registerViewExtension({
212 location: UI.ViewManager.ViewLocationValues.PANEL,
213 id: 'network',
214 commandPrompt: i18nLazyString(UIStrings.showNetwork),
215 title: i18nLazyString(UIStrings.network),
216 order: 40,
217 // This function is executed by the `ViewManager` (defined in `front_end/ui/ViewManager.js`)
218 // whenever the user requests the panel to be loaded.
219 async loadView() {
220 // Lazily load the module, if we hadn't loaded it already.
221 const Network = await loadNetworkModule();
222 // Obtain a singleton reference to the network panel. We don't allow multiple instances
223 // of the same panel, to ensure a performant application.
224 return Network.NetworkPanel.NetworkPanel.instance();
225 },
226});
227
228// A keybinding that is only active when the network panel is open and visible for
229// the user. It can hide the detailed network request information when the user has
230// clicked on a specific network request.
231// For more information about action extensions, please see the documentation in
232// `front_end/ui/ActionRegistration.ts`.
233UI.ActionRegistration.registerActionExtension({
234 actionId: 'network.hide-request-details',
235 category: UI.ActionRegistration.ActionCategory.NETWORK,
236 title: i18nLazyString(UIStrings.hideRequestDetails),
237 // If this function is not defined, this action is considered global.
238 // For more detailed documentation about the definition and usage of `contextTypes`,
239 // please see the `ActionRegistration` interface.
240 contextTypes() {
241 // This will return an array of relevant context types that should be loaded
242 // and visible to the user for this action to be available in the command
243 // menu or when the corresponding keybinding is invoked.
244 return maybeRetrieveContextTypes(Network => [Network.NetworkPanel.NetworkPanel]);
245 },
246 // Just like the network panel, lazily load the action definition when requested by
247 // the user.
248 async loadActionDelegate() {
249 const Network = await loadNetworkModule();
250 return Network.NetworkPanel.ActionDelegate.instance();
251 },
252 bindings: [
253 {
254 shortcut: 'Esc',
255 },
256 ],
257});
258```
259
260The ":meta" `devtools_entrypoint` is added as a dependency to all DevTools application entrypoints defined in [front_end/BUILD.gn](front_end/BUILD.gn).
261
262# Folder structure
263
264DevTools frontend is divided in multiple folders:
265
266- `core/` includes code that can be used by any other module.
267It typically includes utility functions as well as integration with backend.
268- `models/` includes business logic and handling of data received from the backend.
269- `panels/` includes high-level panels and top-level features.
270Each panel typically maps to a separate panel in DevTools, but some panels are integrated into others.
271- `ui/components/` includes reusable components that can be used to build multiple panels.
272- `ui/legacy/components/` includes legacy components.
273Please favor using `ui/components` wherever possible.
274- `entrypoints/` includes all entrypoints of DevTools, which can compose a variety of DevTools panels.
275
276In general, the following structure is applicable to dependencies between modules:
277
278![An overview of allowed visibility rules of modules](./docs/images/module-visibility-rules.png)
279
280- `core/` can be imported by any module
281- `models/` can be imported by `panels/` and `entrypoints/`
282- `ui/` can be imported by `panels/` and `entrypoints/`
283- `panels/` can be imported by `entrypoints/`
284
285To enforce module imports adhere to these rules, actions specify their [GN `visibility` rules].
286An example bundle that is defined in `models/workspace_diff/BUILD.gn`:
287
288```python
289devtools_entrypoint("bundle") {
290 entrypoint = "workspace_diff.ts"
291
292 deps = [ ":workspace_diff" ]
293
294 visibility = [
295 # Allow importing in this same module
296 ":*",
297 # Only these two panels are allowed to use `workspace_diff`
298 "../../panels/changes/*",
299 "../../panels/sources/*",
300 ]
301
302 # Used to allow overrides for downstream projects. Please see below for more information
303 visibility += devtools_models_visibility
304}
305```
306
307There are downstream projects that either have forked DevTools or build on top of DevTools.
308By defining visibility rules using relative paths, in downstream projects there would be no way to update the visibility to allow for additional modules to be importing a module.
309For example, if a downstream project defines a new panel and it would need to depend on `workspace_diff`, it would need to change the visibility definition of `workspace_diff`, which it can't.
310
311To allow for additional visibility rules to be defined, any target in DevTools has to allow for overrides.
312These overrides are typically defined in `visibility.gni` files.
313For example, `models/visibility.gni` defines the following:
314
315```python
316declare_args() {
317 devtools_models_visibility = []
318}
319```
320
321By declaring it as an GN arg, downstream projects can override their GN arg in their `/.gn` file.
322For example, `/.gn` could declare the following:
323
324```python
325default_args = {
326 devtools_models_visibility = [
327 "//front_end/my/new/panel/that/depends/on/workspace_diff/*",
328 ]
329}
330```
331
332Now, the new panel is allowed to add a dependency edge on `models/workspace_diff:bundle`.
333
334# DevTools GRD integration
335
336To bundle DevTools with Chromium, DevTools builds its GRD file that will be consumed by [GRIT].
337The GRD file lists all required files that should be loaded either in Debug or Release mode.
338All files that should be bundled are listed in `config/gni/devtools_grd_files.gni`.
339If a file should be present in debug and release mode, add the file to `grd_files_release_sources`.
340If a file should only be present in debug mode, add the file to `grd_files_debug_sources`.
341
342Note that `devtools_module` and `devtools_entrypoint` automatically take care of this for you.
343Any file included in `devtools_module` is only present in the Debug GRD file.
344Any file included as entrypoint of `devtools_entrypoint` is present in both the Release and Debug GRD file.
345
346There are additional actions that can add files to the GRD file, for example images or Markdown files.
347To allow for any action to define any relevant GRD file inclusions, DevTools uses [GN `metadata` definitions].
348For a custom action, specify the following:
349
350```python
351node_action("generate_css_vars") {
352 <...>
353
354 # The output generated by the action. Not every file listed here is intended to be included
355 # in the GRD bundle. For example, we don't want our TypeScript configuration files to be included.
356 outputs = [
357 "$target_gen_dir/Images.js",
358 "$target_gen_dir/$target_name-tsconfig.json",
359 ]
360
361 data = [ "$target_gen_dir/Images.js" ]
362
363 # Any `metadata` block can define a variable named `grd_files` which takes an `array` of
364 # generated files. The location should be in the `gen/` directory, not the original source
365 # location.
366 #
367 # In this case, we are listing both the generated `Images.js` file, as well as all image
368 # files that DevTools has.
369 metadata = {
370 grd_files = data
371 foreach(_image_file, devtools_image_files) {
372 grd_files += [ "$target_gen_dir/$_image_file" ]
373 }
374 }
375}
376```
377
378The GN metadata is traversed in `/BUILD.gn` and is written to a JSON file in `:input_grd_files`.
379The JSON file is compared to the contents of `:expected_grd_files` which takes the `grd_files_release_sources` and `grd_files_debug_sources` (only in Debug mode) files as input.
380
381If the collected input files matches the expected listed GRD files, the GRD files is written to a GRD file by `:generate_devtools_grd`.
382The GRD file is placed in `gen/third_party/devtools-frontend/src/front_end/` in Chromium.
383
384<!-- Links -->
385
386<!-- Design principles -->
387[Web Platform Design Principles]: https://w3ctag.github.io/design-principles/
388[Put user needs first]: https://w3ctag.github.io/design-principles/#priority-of-constituencies
389[Prefer simple solutions]: https://w3ctag.github.io/design-principles/#simplicity
390[Load only what is necessary]: #load-only-what-is-necessary
391[Prefer web platform features whenever possible]: #prefer-web-platform-features-whenever-possible
392[Design with continuous deployment in mind]: #design-with-continuous-deployment-in-mind
393
394<!-- Web standards -->
395[dynamic imports]: https://v8.dev/features/dynamic-import
396[JavaScript modules]: https://v8.dev/features/modules
397
398<!-- Third-party packages -->
399[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
400[ESLint]: https://eslint.org/
401[GN]: https://gn.googlesource.com/gn/+/main/
402[GN `visibility` rules]: https://gn.googlesource.com/gn/+/main/docs/reference.md#var_visibility
403[GN `metadata` definitions]: https://gn.googlesource.com/gn/+/main/docs/reference.md#var_metadata
404[GRIT]: https://www.chromium.org/developers/tools-we-use-in-chromium/grit/grit-users-guide
405[ndb]: https://github.com/GoogleChromeLabs/ndb
406[Ninja]: https://ninja-build.org/
407[TypeScript]: https://www.typescriptlang.org/