Steps to reproduce
- Run a Flutter app on Windows in profile mode with
flutter run -d windows --profile --dart-flags=--sample-buffer-duration=120.
- Open Flutter DevTools in the browser (I used Chrome).
- Open the performance screen.
- Click
Clear All.
- Open CPU Profiler.
- Click
Start recording (sampling rate set to the default Medium).
- Use the application normally for approximately 2 minutes.
- Go back to the browser with DevTools open and click
Stop recording.
- From the CPU Profiler screen, at the top right click the arrow with the tooltip
Save this screen's data for offline viewing.
- After that file is saved, go to the performance screen.
- At the top right click the arrow with the tooltip
Save this screen's data for offline viewing.
What should happen
I should quickly and easily get the performance trace file ready to be saved, without DevTools needing massive amounts of RAM to do this and without anything crashing.
What happens instead
DevTools starts using really huge amounts of RAM, and then the Chrome tab crashes with an out-of-memory error. In the tests using Chrome, I was not even able to save the performance data before the crash (tried doing it 4 times).
I also repeated the same test using DevTools embedded inside vs code, this time keeping track of the RAM usage more closely. This test lasted slightly longer than the Chrome one, closer to 2 minutes and 30 seconds, instead of around 2 minutes for Chrome.
I was able to save the CPU profiling data for offline viewing easily in both tests, so the OOM problem happens only when trying to save the performance screen data.
With DevTools embedded inside vs code, I was able to save the performance data for offline viewing. The file in the end was just a 184 MB json. I have no idea if the vs code webview simply has more memory headroom than Chrome or if something else is different, but it seems that it was able to stay responsive/survive long enough to complete the save. It still ended up becoming unresponsive and it crashed vs code some seconds after the export finished.
The RAM usage during this embedded in vs code test was:
- VS Code before opening DevTools: about 1.6 GB
- After opening DevTools: about 2.0 GB
- At the end of the CPU profiling recording: about 3.6 GB
- Immediately after saving the CPU profiling data: about 3.8 GB
- While saving the Performance screen data: about 6.4 GB
- After the Performance data had been saved: about 3.2 GB, and then it crashed after some more seconds.
During the save data for offline viewing operation, VS Code also showed a "page unresponsive" warning a bit before crashing.
Vs code went from about 1.6 GB of RAM without DevTools open to about 6.4 GB while saving performance data for a simple 2 minutes and 30 seconds test.. So I don't think it matters much that I was able to export the data for offline viewing from the embedded version of devtools, because clearly this is not how it should be working.
More details
- This whole thing of having DevTools as a web app / embedded webview seems to me really not ideal. I normally do not want to have stuff that uses such big amounts of resources opened while profiling an app.
- Even if you feel that having DevTools as a web app / embedded webview is the best solution (which I struggle to believe), DevTools is missing a proper way to get normal structured performance data that can be used by AI, automation or other tools. I'll open a separate issue about that, since it's a different problem.
For what concerns this issue, it seems to me that saving the performance data for offline viewing appears to be extremely memory inefficient, and the way the Perfetto binary is serialized seems to be one of the primary causes.
The fact that with the embedded version it was able to complete the save operation while Chrome crashed before completing it does not really matter much because in both cases the operation required a very large amount of memory, and RAM usage spiked by many GB during the saving of the file and in both cases it ended with a crash.
Looking at the source code of DevTools, the performance trace is stored in memory as a Uint8List:
performance_model.dart, line 51
final Uint8List? perfettoTraceBinary;
That Uint8List is then placed directly into the JSON-serializable map:
performance_model.dart, lines 70-77
Map<String, Object?> toJson() => {
traceBinaryKey: perfettoTraceBinary,
flutterFramesKey: frames.map((frame) => frame.json).toList(),
selectedFrameIdKey: selectedFrame?.id,
displayRefreshRateKey: displayRefreshRate,
rebuildCountModelKey: rebuildCountModel?.toJson(),
selectedTabKey: selectedTab,
};
This results in exported data like:
"traceBinary":[10,18,80,1,50,14,16,3,...]
The importer confirms that this is just a JSON list of integers that gets converted back into a Uint8List:
performance_model.dart, lines 80-85
Uint8List? get traceBinary {
final value = (json[OfflinePerformanceData.traceBinaryKey] as List?)
?.cast<int>();
return value == null ? null : Uint8List.fromList(value);
}
The performance snapshot takes the complete perfetto trace:
performance_controller.dart, lines 281-291
OfflineScreenData prepareOfflineScreenData() => OfflineScreenData(
screenId: PerformanceScreen.id,
data: OfflinePerformanceData(
perfettoTraceBinary: timelineEventsController.fullPerfettoTrace,
frames: flutterFramesController.flutterFrames.value,
selectedFrame: flutterFramesController.selectedFrame.value,
rebuildCountModel: rebuildCountModel,
displayRefreshRate: flutterFramesController.displayRefreshRate.value,
selectedTab: selectedFeatureTabIndex,
).toJson(),
);
fullPerfettoTrace itself merges the trace ring buffer into a single Uint8List:
timeline_events_controller.dart, lines 61-65
/// The complete Perfetto timeline that DevTools has received from the VM.
///
/// This returns the merged value of all the traces in [traceRingBuffer],
/// which is periodically trimmed to preserve memory in DevTools.
Uint8List get fullPerfettoTrace => traceRingBuffer.merged;
Interestingly, the comments immediately below that explicitly say the trace is kept in a ring buffer to prevent DevTools from running out of memory:
timeline_events_controller.dart, lines 67-80
/// A ring buffer containing all the Perfetto trace binaries that we have
/// received from the VM.
///
/// This ring buffer is built up by polling every [_timelinePollingInterval]
/// and fetching new Perfetto timeline data from the VM.
///
/// We use a ring buffer for this data so that the earliest entries will be
/// removed when the total size of this queue exceeds [_traceRingBufferSize].
/// This prevents the Performance page from causing DevTools to OOM.
However the max size of that ring buffer is:
timeline_events_controller.dart, lines 84-89
/// Size limit for [traceRingBuffer] that determines when traces should be
/// removed from the queue.
///
/// Wasm sets a size limit on byte arrays of int32 max which is specifically
/// 1 less than 1 << 31.
final _traceRingBufferSize = (1 << 31) - 1;
So the trace buffer is configured with a maximum size of about 2.15 GB
The offline export then builds the complete encoded data before download starts:
offline_data.dart, lines 189-196
/// Exports the current screen data to a .json file and downloads the file to
/// the user's Downloads directory.
void exportData() {
final encodedData = _exportController.encode(
prepareOfflineScreenData().toJson(),
);
_exportController.downloadFile(encodedData);
}
The entire snapshot is then converted into one JSON String using jsonEncode:
import_export.dart, lines 175-178
String encode(Map<String, Object?> offlineScreenData) {
final data = generateDataForExport(offlineScreenData: offlineScreenData);
return jsonEncode(data, toEncodable: toEncodable);
}
The complete JSON String is then put into a JavaScript Blob for the download:
_export_web.dart, lines 20-37
void saveFile<T>({required T content, required String fileName}) {
final element = document.createElement('a') as HTMLAnchorElement;
final Blob blob;
if (content is String) {
blob = Blob([content.toJS].toJS);
} else if (content is Uint8List) {
blob = Blob([content.toJS].toJS);
} else {
throw 'Unsupported content type: $T';
}
element.setAttribute('href', URL.createObjectURL(blob));
element.setAttribute('download', fileName);
element.style.display = 'none';
(document.body as HTMLBodyElement).append(element);
element.click();
element.remove();
}
So as far as I understand, in the current code, performance export for offline viewing works approximately like this:
perfetto trace chunks
→ merged Uint8List
→ JSON array containing every binary byte as a decimal integer
→ complete encoded JSON String
→ JavaScript Blob
→ download
This seems extremely memory-inefficient.
The Perfetto trace already exists as binary data. Turning every byte into decimal numbers inside JSON makes the data a lot larger (I tried doing some quick math and, unless I made some mistakes in the calculation, depending on byte values, the trace data could end up being about 2 to 4 times larger in the saved file). On top of that, the export creates the merged Uint8List and then the complete JSON String before the download starts.
It also seems quite contradictory that the code itself explicitly says that the ring buffer exists to prevent the performance page from running out of memory, while saving that same data requires building a significantly larger JSON version of it in memory before the download starts..
When saving the performance screen data for offline viewing, I think the Perfetto trace should stay as binary data instead of being converted into a huge JSON array of integers.
The download could then be just a zip containing the JSON data and the Perfetto trace as a separate binary file, something like:
performance_data.zip
├── metadata.json
└── trace.perfetto
Environment
- Ram: 16 GB
- CPU: Ryzen 5 1600
- Chrome version during the test: 152.0.7977.83 (Official Build) (64-bit)
Chrome has since automatically updated to 153.0.8010.48, which is why flutter doctor -v shows a newer version.
- Vs code version 1.138.0
I did not have anything else opened while trying to export profiling data other than vs code, my app and chrome (with only devtools tab opened). And in the test with DevTools embedded in vs code, I only had vs code and my app opened. For context, my app uses only about 400 MB RAM.
Doctor output
flutter doctor -v
[√] Flutter (Channel master, 3.48.0-1.0.pre-787, on Microsoft Windows [Version 10.0.19045.7725], locale it-IT) [3,1s]
• Flutter version 3.48.0-1.0.pre-787 on channel master at C:\src\flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 154e6f34f6 (31 hours ago), 2026-09-16 12:08:31 +0000
• Engine revision 154e6f34f6
• Dart version 3.14.0 (build 3.14.0-233.0.dev)
• DevTools version 2.61.0-dev.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager,
omit-legacy-version-file, enable-windowing, enable-lldb-debugging, enable-uiscene-migration, enable-riscv64, enable-hcpp
[√] Windows Version (10 Pro 64-bit, 22H2, 2009) [2,1s]
[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [3,4s]
• Android SDK at C:\Users\moret\AppData\Local\Android\sdk
• Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
• Platform android-36.1, build-tools 36.1.0
• Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.10+-14961533-b1163.108)
• All Android licenses accepted.
[√] Chrome - develop for the web [141ms]
• Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.14.40) [139ms]
• Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community
• Visual Studio Community 2022 version 17.14.37628.2
• Windows 10 SDK version 10.0.26100.0
[√] Connected device (3 available) [304ms]
• Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.19045.7725]
• Chrome (web) • chrome • web-javascript • Google Chrome 153.0.8010.48
• Edge (web) • edge • web-javascript • Microsoft Edge 153.0.4234.32
[√] Network resources [792ms]
• All expected network resources are available.
• No issues found!
I want to underline that the title of this issue says "Exporting a large trace..", but the only thing that was large about the trace I was trying to export was its size. The actual profiling time was only between 1 and a half to 2 and a half minutes, which I'd say is not a long profiling session.
Apart from this specific OOM/export issue, I think the current DevTools workflow could be improved in a much more significant way, especially for AI use, automation and command line use, which are absolutely key things nowadays (analyzing vast amounts of data is exactly what AI is great at).
I'll open a separate issue with a proposal for that too sometime next week.
Steps to reproduce
flutter run -d windows --profile --dart-flags=--sample-buffer-duration=120.Clear All.Start recording(sampling rate set to the defaultMedium).Stop recording.Save this screen's data for offline viewing.Save this screen's data for offline viewing.What should happen
I should quickly and easily get the performance trace file ready to be saved, without DevTools needing massive amounts of RAM to do this and without anything crashing.
What happens instead
DevTools starts using really huge amounts of RAM, and then the Chrome tab crashes with an out-of-memory error. In the tests using Chrome, I was not even able to save the performance data before the crash (tried doing it 4 times).
I also repeated the same test using DevTools embedded inside vs code, this time keeping track of the RAM usage more closely. This test lasted slightly longer than the Chrome one, closer to 2 minutes and 30 seconds, instead of around 2 minutes for Chrome.
I was able to save the CPU profiling data for offline viewing easily in both tests, so the OOM problem happens only when trying to save the performance screen data.
With DevTools embedded inside vs code, I was able to save the performance data for offline viewing. The file in the end was just a 184 MB json. I have no idea if the vs code webview simply has more memory headroom than Chrome or if something else is different, but it seems that it was able to stay responsive/survive long enough to complete the save. It still ended up becoming unresponsive and it crashed vs code some seconds after the export finished.
The RAM usage during this embedded in vs code test was:
During the save data for offline viewing operation, VS Code also showed a "page unresponsive" warning a bit before crashing.
Vs code went from about 1.6 GB of RAM without DevTools open to about 6.4 GB while saving performance data for a simple 2 minutes and 30 seconds test.. So I don't think it matters much that I was able to export the data for offline viewing from the embedded version of devtools, because clearly this is not how it should be working.
More details
For what concerns this issue, it seems to me that saving the performance data for offline viewing appears to be extremely memory inefficient, and the way the Perfetto binary is serialized seems to be one of the primary causes.
The fact that with the embedded version it was able to complete the save operation while Chrome crashed before completing it does not really matter much because in both cases the operation required a very large amount of memory, and RAM usage spiked by many GB during the saving of the file and in both cases it ended with a crash.
Looking at the source code of DevTools, the performance trace is stored in memory as a
Uint8List:performance_model.dart, line 51That
Uint8Listis then placed directly into the JSON-serializable map:performance_model.dart, lines 70-77This results in exported data like:
The importer confirms that this is just a JSON list of integers that gets converted back into a
Uint8List:performance_model.dart, lines 80-85The performance snapshot takes the complete perfetto trace:
performance_controller.dart, lines 281-291fullPerfettoTraceitself merges the trace ring buffer into a singleUint8List:timeline_events_controller.dart, lines 61-65Interestingly, the comments immediately below that explicitly say the trace is kept in a ring buffer to prevent DevTools from running out of memory:
timeline_events_controller.dart, lines 67-80However the max size of that ring buffer is:
timeline_events_controller.dart, lines 84-89So the trace buffer is configured with a maximum size of about 2.15 GB
The offline export then builds the complete encoded data before download starts:
offline_data.dart, lines 189-196The entire snapshot is then converted into one JSON String using
jsonEncode:import_export.dart, lines 175-178The complete JSON String is then put into a JavaScript Blob for the download:
_export_web.dart, lines 20-37So as far as I understand, in the current code, performance export for offline viewing works approximately like this:
This seems extremely memory-inefficient.
The Perfetto trace already exists as binary data. Turning every byte into decimal numbers inside JSON makes the data a lot larger (I tried doing some quick math and, unless I made some mistakes in the calculation, depending on byte values, the trace data could end up being about 2 to 4 times larger in the saved file). On top of that, the export creates the merged Uint8List and then the complete JSON String before the download starts.
It also seems quite contradictory that the code itself explicitly says that the ring buffer exists to prevent the performance page from running out of memory, while saving that same data requires building a significantly larger JSON version of it in memory before the download starts..
When saving the performance screen data for offline viewing, I think the Perfetto trace should stay as binary data instead of being converted into a huge JSON array of integers.
The download could then be just a zip containing the JSON data and the Perfetto trace as a separate binary file, something like:
Environment
Chrome has since automatically updated to 153.0.8010.48, which is why flutter doctor -v shows a newer version.
I did not have anything else opened while trying to export profiling data other than vs code, my app and chrome (with only devtools tab opened). And in the test with DevTools embedded in vs code, I only had vs code and my app opened. For context, my app uses only about 400 MB RAM.
Doctor output
I want to underline that the title of this issue says "Exporting a large trace..", but the only thing that was large about the trace I was trying to export was its size. The actual profiling time was only between 1 and a half to 2 and a half minutes, which I'd say is not a long profiling session.
Apart from this specific OOM/export issue, I think the current DevTools workflow could be improved in a much more significant way, especially for AI use, automation and command line use, which are absolutely key things nowadays (analyzing vast amounts of data is exactly what AI is great at).
I'll open a separate issue with a proposal for that too sometime next week.