Watch the video (10 min)
On this page25 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is Dart DevTools?
  5. 2. Starting a Program with --observe
  6. 3. Reading the Startup Lines
  7. 4. Paused at Start
  8. 5. The dart devtools Command
  9. 6. The Debugger Page
  10. 7. The Logging Page and log()
  11. 8. The CPU Profiler: Finding a Hot Function
  12. 9. Fixing the Hot Function
  13. 10. The Memory Page: Spotting a Growing List
  14. 11. Fixing the Leak
  15. 12. The Network Page
  16. 13. A Real-World Example
  17. Code Examples
  18. Common Beginner Mistakes
  19. Best Practices
  20. Mini Challenge
  21. Practice Questions
  22. Practice Answers
  23. Quick Revision
  24. Key Takeaways
  25. What's Next?

Lecture 116 — DevTools

Category19 — Dart Developer Tools
Lecture116 of 140
Estimated video length10 min
TargetDart 3.12.2
LevelAdvanced

A debugger shows what a program is doing at one moment. DevTools also shows where it spends its time and what fills its memory while it runs.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every terminal line in this lecture is real, produced on Dart 3.12.2. The secret part of each address is shortened to …. DevTools screens show different numbers on every machine, so this lecture describes what you will see rather than quoting figures.


Prerequisites

Students should already understand, from Lectures 001–115:

  • classes, lists and Future.delayed (Lectures 053, 039, 088)
  • breakpoints, log() and dart run --observe (Lecture 115)

Connection With Previous Lectures

In Lecture 115 we paused programs on breakpoints, and dart run --observe printed a DevTools link we did not open yet. A breakpoint answers "why is this value wrong?" Two other questions are just as common:

  • "My program gives the right answer, but it is slow. Which function is to blame?"
  • "My program runs for hours and keeps getting bigger. What is filling the memory?"

A breakpoint cannot answer either. You need a tool that watches the program at full speed.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what Dart DevTools is and how it connects to a running program
  • Start a program with dart run --observe and open DevTools from its link
  • Start DevTools on its own with dart devtools
  • Set a breakpoint in the DevTools Debugger page
  • Read log() messages on the Logging page
  • Find a hot function with the CPU Profiler
  • Spot a growing list with the Memory page
  • Recognize what the Network page is for

1. What Is Dart DevTools?

Dart DevTools is a set of debugging and performance tools that runs in your web browser. It connects to a running Dart program and shows its code, its CPU use, its memory and its log messages.

Analogy — DevTools is like a car's dashboard: the car keeps driving while the gauges show speed, fuel and engine temperature.

The program needs no special code — only the right start-up flag.


2. Starting a Program with --observe

Add --observe between dart run and the file name. Here is a program that ticks five times:

dart
import 'dart:async';

void main() {
  var count = 0;
  Timer.periodic(Duration(seconds: 1), (timer) {
    count++;
    print('Tick $count');
    if (count == 5) timer.cancel();
  });
}
output
$ dart run --observe ticker.dart
The Dart VM service is listening on http://127.0.0.1:8181/…/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/…/devtools/?uri=ws://127.0.0.1:8181/…/ws
Tick 1
Tick 2
Tick 3
Tick 4
Tick 5
vm-service: isolate(…)  'main' has no debugger attached and is paused at exit.  Connect to the Dart VM service at http://127.0.0.1:8181/…/ to debug.

3. Reading the Startup Lines

  • The VM service is a small server inside the running program that DevTools talks to. Its default port is 8181.
  • The DevTools link opens DevTools in your browser, already connected. Copy it, or Cmd/Ctrl-click it in the terminal.
  • The part shown as … is a random secret created on every run. Anyone with the full address can control the program, so never share it.
  • Paused at exit — a finished program waits so you can still inspect it. It never returns to the prompt by itself; press Ctrl+C.

4. Paused at Start

A short program finishes before you can even open the link. Ask Dart to wait before main() runs:

output
$ dart run --observe --pause-isolates-on-start marks.dart
vm-service: isolate(…) 'main' has no debugger attached and is paused at start.  Connect to the Dart VM service to debug.
The Dart VM service is listening on http://127.0.0.1:8181/…/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/…/devtools/?uri=ws://127.0.0.1:8181/…/ws

Open the link, set breakpoints, then press Resume. (An isolate runs your code — Lecture 098.)


5. The dart devtools Command

You can also start DevTools by itself:

output
$ dart devtools --help
Open DevTools (optionally connecting to an existing application).

Usage: dart devtools [arguments] [service protocol uri]
    --version                         Prints the DevTools version.
    --port=<port>                     Port to serve DevTools on; specify 0 to automatically use any available port.
                                      (defaults to "9100")
    --[no-]launch-browser             Launches DevTools in a browser immediately at start.
                                      (defaults to on unless in --machine mode)
output
$ dart devtools
Serving DevTools at http://127.0.0.1:9100.

          Hit ctrl-c to terminate the server.

A browser tab opens asking for an address. Paste the VM service address from an --observe run and press Connect, or pass it directly: dart devtools http://127.0.0.1:8181/…/.


6. The Debugger Page

The Debugger page is the same debugger from Lecture 115, inside the browser. Run this program with --pause-isolates-on-start:

dart
double average(List<int> marks) {
  var total = 0;
  for (var mark in marks) {
    total += mark;
  }
  return total / marks.length;
}

void main() {
  print('Ali: ${average([80, 75, 85])}');
  print('Sara: ${average([90, 85, 95])}');
}
output
Ali: 80.0
Sara: 90.0

Open marks.dart in the file list on the left, click the line number beside total += mark; to set a breakpoint, then press Resume. The program stops there; the Variables panel shows marks, mark and total, and the step buttons move one line at a time.

Warning — do not reach for debugger() here. As Lecture 115 showed, a plain dart run that reaches debugger() freezes forever. Click breakpoints in DevTools instead.


7. The Logging Page and log()

In Lecture 115, log() printed nothing in the terminal, with or without --observe. This page is where those messages go:

dart
import 'dart:developer';

void main() {
  print('Shown in the terminal');
  log('Shown in the Logging page', name: 'shop');
}
output
Shown in the terminal

DevTools shows log events that happen while it is connected, so start this one with --pause-isolates-on-start, open the Logging page, then press Resume. The shop message appears next to the program's stdout lines and garbage-collection (gc) events.


8. The CPU Profiler: Finding a Hot Function

This program is correct but slow. Run it with --observe:

dart
bool isPrime(int n) {
  if (n < 2) return false;
  for (var i = 2; i < n; i++) {
    if (n % i == 0) return false;
  }
  return true;
}

int countPrimes(int limit) {
  var count = 0;
  for (var n = 2; n <= limit; n++) {
    if (isPrime(n)) count++;
  }
  return count;
}

String formatRound(int round, int primes) => 'Round $round: $primes primes';

void main() {
  for (var round = 1; round <= 50; round++) {
    var primes = countPrimes(100000);
    print(formatRound(round, primes));
  }
  print('Done');
}
output
Round 1: 9592 primes
Round 2: 9592 primes
...
Round 50: 9592 primes
Done

On the CPU Profiler page, press Record, wait a few rounds, then Stop (or, after it finishes and pauses at exit, Load all CPU samples).

In the Bottom Up table, sort by Self Time — time spent in the function's own code. Total time also includes the functions it calls. isPrime sits at the top with nearly all the self time; countPrimes has a large total but little self time, and formatRound barely appears. The CPU Flame Chart tab shows the same: the widest bar is the hottest.


9. Fixing the Hot Function

A number that has a divisor always has one no bigger than its square root, so the loop can stop much earlier. Change one condition in isPrime:

dart
bool isPrime(int n) {
  if (n < 2) return false;
  for (var i = 2; i * i <= n; i++) {
    if (n % i == 0) return false;
  }
  return true;
}

Run it again: the same 50 lines appear almost instantly. The workflow is profile first, then fix the function the profiler blames — not the one you guessed.


10. The Memory Page: Spotting a Growing List

This program stores every visit and never removes any:

dart
class Visit {
  Visit(this.user, this.page);

  final String user;
  final String page;
}

final history = <Visit>[];

void recordVisit(int id) {
  history.add(Visit('Sara', '/products/$id'));
}

Future<void> main() async {
  for (var second = 1; second <= 60; second++) {
    for (var id = 0; id < 20000; id++) {
      recordVisit(id);
    }
    print('Second $second: ${history.length} visits stored');
    await Future.delayed(Duration(seconds: 1));
  }
}
output
Second 1: 20000 visits stored
Second 2: 40000 visits stored
Second 3: 60000 visits stored

Run it with --observe and open the Memory page. Its chart shows the Dart heap — the memory holding your objects — climbing step by step instead of levelling off.

A heap snapshot lists every live object, grouped by class. On Diff Snapshots, take one snapshot, wait a few seconds, take another, and compare: Visit grows between the two. Select it and its retaining path shows what keeps the objects alive — the top-level history list.


11. Fixing the Leak

A memory leak is memory kept but no longer needed. Keep only the newest entries:

dart
const maxHistory = 1000;

void recordVisit(int id) {
  history.add(Visit('Sara', '/products/$id'));
  if (history.length > maxHistory) {
    history.removeAt(0);
  }
}

With these lines replacing the old recordVisit (and the loop shortened to 3 seconds):

output
Second 1: 1000 visits stored
Second 2: 1000 visits stored
Second 3: 1000 visits stored

Profiled again, the heap rises and falls as Dart frees old Visit objects.


12. The Network Page

The Network page lists HTTP requests made with dart:io's HttpClient (and packages built on it): the address, status, timing, and request and response bodies. Our programs make no requests, so it stays empty for now. Other pages, such as Performance, matter mostly for Flutter apps. We return to speed and memory in Lecture 126 — Performance & Memory.


13. A Real-World Example

Sara's shop service has a slow function, a growing list and log messages:

dart
import 'dart:developer';

class Order {
  Order(this.customer, this.total);

  final String customer;
  final double total;
}

final allOrders = <Order>[];

int checksum(String text) {
  var sum = 0;
  for (var i = 0; i < 200000; i++) {
    sum = (sum + text.codeUnitAt(i % text.length) * i) % 1000003;
  }
  return sum;
}

Future<void> main() async {
  var customers = ['Ali', 'Sara', 'Zayan'];
  for (var batch = 1; batch <= 60; batch++) {
    for (var i = 0; i < 300; i++) {
      var order = Order(customers[i % 3], 99.5 + i);
      checksum(order.customer);
      allOrders.add(order);
    }
    log('Batch $batch: ${allOrders.length} orders', name: 'shop');
    print('Batch $batch done');
    await Future.delayed(Duration(milliseconds: 200));
  }
}
output
Batch 1 done
Batch 2 done
...

Run dart run --observe shop_service.dart and open the link:

  1. Logging — filtered by shop, the order count rises by 300 each batch.
  2. CPU Profiler — checksum dominates self time.
  3. Memory — a snapshot diff shows Order growing, kept alive by allOrders.

Code Examples

Example 1 — A Short Program with --observe

dart
void main() {
  print('Hello from Ali');
}
output
$ dart run --observe hello.dart
Hello from Ali
vm-service: isolate(…)  'main' has no debugger attached and is paused at exit.  Connect to the Dart VM service to debug.
The Dart VM service is listening on http://127.0.0.1:8181/…/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/…/devtools/?uri=ws://127.0.0.1:8181/…/ws

It finished at once, so the greeting even prints before the service lines. Ctrl+C ends it.

Example 2 — Choosing Another Port

output
$ dart run --observe=8282 ticker.dart
The Dart VM service is listening on http://127.0.0.1:8282/…/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8282/…/devtools/?uri=ws://127.0.0.1:8282/…/ws
Tick 1

Example 3 — Checking the DevTools Version

output
$ dart devtools --version
Dart DevTools version 2.57.0

DevTools ships inside the SDK.

Example 4 — DevTools Without Opening a Browser

output
$ dart devtools --no-launch-browser
Serving DevTools at http://127.0.0.1:9100.

          Hit ctrl-c to terminate the server.

Open http://127.0.0.1:9100 yourself.

Example 5 — Pausing a Quick Program at Start

dart run --observe --pause-isolates-on-start marks.dart prints the paused at start line from section 4; nothing from main() prints until you press Resume.

Example 6 — Bank Messages in the Logging Page

dart
import 'dart:developer';

void main() {
  var balance = 1000.0;
  for (var amount in [250.5, -100, -2000]) {
    if (balance + amount < 0) {
      log('Rejected $amount, balance is $balance', name: 'bank', level: 900);
      continue;
    }
    balance += amount;
    log('Applied $amount', name: 'bank');
  }
  print('Balance: $balance');
}
output
Balance: 1150.5

Run it with --observe --pause-isolates-on-start, open Logging, press Resume: three bank messages appear, and level: 900 marks the rejection as a warning.

Example 7 — Student Marks: A Slow Sort to Profile

dart
List<int> makeMarks(int count) => [for (var i = 0; i < count; i++) i * 37 % 101];

void bubbleSort(List<int> marks) {
  for (var i = 0; i < marks.length; i++) {
    for (var j = 0; j < marks.length - 1 - i; j++) {
      if (marks[j] > marks[j + 1]) {
        var temp = marks[j];
        marks[j] = marks[j + 1];
        marks[j + 1] = temp;
      }
    }
  }
}

void main() {
  for (var round = 1; round <= 30; round++) {
    var marks = makeMarks(20000);
    bubbleSort(marks);
    print('Round $round: lowest ${marks.first}, highest ${marks.last}');
  }
}
output
Round 1: lowest 0, highest 100
...
Round 30: lowest 0, highest 100

Profiled, bubbleSort owns almost all the self time.

Example 8 — Student Marks: The Fix

dart
List<int> makeMarks(int count) => [for (var i = 0; i < count; i++) i * 37 % 101];

void main() {
  for (var round = 1; round <= 30; round++) {
    var marks = makeMarks(20000);
    marks.sort();
    print('Round $round: lowest ${marks.first}, highest ${marks.last}');
  }
}
output
Round 1: lowest 0, highest 100
...
Round 30: lowest 0, highest 100

Same output, a fraction of the time.

Example 9 — Temperature Readings: A Leak to Find

dart
final readings = <double>[];

Future<void> main() async {
  for (var second = 1; second <= 120; second++) {
    for (var i = 0; i < 50000; i++) {
      readings.add(20 + i % 10 / 2);
    }
    print('Second $second: ${readings.length} readings kept');
    await Future.delayed(Duration(seconds: 1));
  }
}
output
Second 1: 50000 readings kept
Second 2: 100000 readings kept
Second 3: 150000 readings kept

The heap climbs every second for two minutes.

Example 10 — Temperature Readings: A Rolling Window

dart
const window = 60;
final readings = <double>[];

void addReading(double celsius) {
  readings.add(celsius);
  if (readings.length > window) {
    readings.removeAt(0);
  }
}

Future<void> main() async {
  for (var second = 1; second <= 3; second++) {
    for (var i = 0; i < 50000; i++) {
      addReading(20 + i % 10 / 2);
    }
    print('Second $second: ${readings.length} readings kept');
    await Future.delayed(Duration(seconds: 1));
  }
}
output
Second 1: 60 readings kept
Second 2: 60 readings kept
Second 3: 60 readings kept

Common Beginner Mistakes

1. Two programs on the same port

Starting a second --observe program while the first still runs:

output
Could not start the VM service: DartDevelopmentServiceException: Failed to create server socket (Address already in use): 127.0.0.1:8181
Tick 1

It still runs, but without DevTools. Stop the first program or use --observe=8282.

2. Waiting for a finished program to close — with --observe it pauses at exit. Press Ctrl+C.

3. Opening the link too late — a quick program finishes before you connect. Add --pause-isolates-on-start.

4. Optimizing by guesswork — rewriting formatRound in section 8 would change nothing. Let the profiler name the hot function first.

5. Leaving debugger() in a program — a plain dart run that reaches it freezes (Lecture 115). Use breakpoints in the Debugger page.


Best Practices

  • Never share the VM service or DevTools address — the token gives full control.
  • Sort the CPU Profiler by self time to find the function doing the work.
  • Compare two heap snapshots; one snapshot cannot show growth.
  • Give long-lived lists a maximum size.

Mini Challenge

Zayan's score tracker keeps every score forever. Change it so scores never holds more than 5 entries, then print its length after each round.

dart
final scores = <int>[];

void main() {
  for (var round = 1; round <= 3; round++) {
    for (var i = 0; i < 10; i++) {
      scores.add(round * 10 + i);
    }
    print('Round $round: ${scores.length} scores');
  }
}

Try it yourself before opening the solution.

Show solution
dart
const maxScores = 5;
final scores = <int>[];

void addScore(int score) {
  scores.add(score);
  if (scores.length > maxScores) {
    scores.removeAt(0);
  }
}

void main() {
  for (var round = 1; round <= 3; round++) {
    for (var i = 0; i < 10; i++) {
      addScore(round * 10 + i);
    }
    print('Round $round: ${scores.length} scores');
  }
  print(scores);
}
output
Round 1: 5 scores
Round 2: 5 scores
Round 3: 5 scores
[35, 36, 37, 38, 39]

addScore removes the oldest score as soon as the list passes 5.


Practice Questions

Beginner

  1. Which command runs app.dart so DevTools can connect to it?
  2. What are the two addresses printed at startup, and which one do you paste into dart devtools?
  3. Which command prints the installed DevTools version?

Application

  1. Run ticker.dart with the VM service on port 9000. Write the command.

  2. Your program finishes instantly. How do you set a breakpoint before main() runs?

  3. Change this so the message goes to the Logging page under the name marks, not the terminal:

    dart
    void main() {
      print('Ali scored 240');
    }
  4. In the CPU Profiler, function A has a large total time but small self time; function B has a large self time. Which one should you optimize first, and why?

Challenge

  1. Run Example 7 with --observe, record a profile, and name the hot function. Then explain why Example 8's change removed it.
  2. A program's heap chart climbs for ten minutes. Describe the steps on the Memory page that reveal which class is growing and what keeps it alive.
  3. Run Example 9 with --observe, then Example 10. Describe how the Memory chart differs and which class a snapshot diff points to in Example 9.

Practice Answers

Question 1 — Solution

dart run --observe app.dart

Question 2 — Solution

The VM service address (http://127.0.0.1:8181/…/) and the DevTools address (which opens DevTools directly). Paste the VM service address into dart devtools.

Question 3 — Solution

output
$ dart devtools --version
Dart DevTools version 2.57.0

Question 4 — Solution

dart run --observe=9000 ticker.dart

Question 5 — Solution

dart run --observe --pause-isolates-on-start app.dart, open the link, set the breakpoint in the Debugger page, then press Resume.

Question 6 — Solution

dart
import 'dart:developer';

void main() {
  log('Ali scored 240', name: 'marks');
}

The terminal stays empty; the message appears on the Logging page.

Question 7 — Solution

Function B — its own code uses the time. A is slow only because it calls something slow.

Question 8 — Solution

bubbleSort. Its nested loops compare almost every pair of marks; sort() uses a far more efficient algorithm.

Question 9 — Solution

Take a heap snapshot, wait, take a second one, and diff them in Diff Snapshots. The class with the biggest increase is growing. Select it and read its retaining path to see which variable keeps the objects alive.

Question 10 — Solution

In Example 9 the heap chart climbs every second and never levels off; a snapshot diff shows the list's contents growing, kept alive by the top-level readings list. In Example 10 the chart stays flat, because the list never holds more than 60 readings.


Quick Revision

  1. What does DevTools connect to inside a running program?
  2. What is the default VM service port?
  3. Why does an --observe program not exit on its own?
  4. What is the difference between self time and total time?
  5. What is a heap snapshot?

Key Takeaways

  • DevTools is a browser-based debugger and profiler for a running program.
  • dart run --observe starts the VM service and prints a ready-to-open DevTools link.
  • An --observe program pauses at exit; --pause-isolates-on-start pauses before main().
  • dart devtools serves DevTools on port 9100; paste a VM service address to connect.
  • The CPU Profiler finds hot functions — sort by self time.
  • The Memory page shows heap growth; diff two snapshots to find the growing class.
  • log() from dart:developer feeds the Logging page.
  • The Network page shows HTTP traffic made with HttpClient.

What's Next?

Lecture 117 — Linting & Effective Dart. DevTools finds problems while a program runs, but many can be caught before it runs at all. Next we turn on lint rules and follow the Effective Dart style guide, so the analyzer warns about risky or unclear code as you type.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.