devtools icon indicating copy to clipboard operation
devtools copied to clipboard

DevTools performance page doesn't detect jank between frames

Open kenzieschmoll opened this issue 3 years ago • 7 comments

In this example, the running app (Wonderous in profile mode) visibly janks, but there are no janky frames in the DevTools performance page. Upon looking deeper into the timeline trace, it is clear that there is a large gap of latency between two frames: Screen Shot 2022-09-15 at 11 47 36 AM Screen Shot 2022-09-15 at 11 48 03 AM

How is a user supposed to debug the jank at this point? How can we help them understand what could be causing the latency between frames?

CC @chinmaygarde @dnfield @iskakaushik @jonahwilliams for ideas.

kenzieschmoll avatar Sep 15 '22 18:09 kenzieschmoll

One idea is to keep a running average of all the 'VSYNC' event times we have seen, and to tell users that we detected a longer 'VSYNC' than normal. It is still unclear what we can tell them for why this happened and what they can do to fix it, though.

kenzieschmoll avatar Sep 15 '22 19:09 kenzieschmoll

Can you attach a systrace or observatory trace for that? That should tell us more about whats happening and how we can fill in the blanks.

chinmaygarde avatar Sep 15 '22 19:09 chinmaygarde

Here's an Observatory trace dart-timeline-2022-9-15.json.txt

kenzieschmoll avatar Sep 15 '22 23:09 kenzieschmoll

That seems to be a wait for the GPU to finish work. We usually capture and instrument a GPU frame for such cases. We could add a trace around the wait to indicate to the user that the CPU is workload is fine but we are waiting for the GPU to catch up.

chinmaygarde avatar Sep 15 '22 23:09 chinmaygarde

That would be helpful. Especially if that message is clear by the event name. So the performance solution here would be to optimize what is happening on the raster thread?

kenzieschmoll avatar Sep 16 '22 15:09 kenzieschmoll

So the performance solution here would be to optimize what is happening on the raster thread?

Unfortunately, it's not quite that simple. It could be that the application has specified a particularly expensive operation (like a non-pipeline blend). There is nothing we can do on the raster thread in that case and the application must be reworked. With Impeller, we are putting more stuff in the GPU traces but we can only capture them currently using Xcode. Would it be useful to walk over our tracing workflow to see how and what we can expose via DevTools?

chinmaygarde avatar Sep 16 '22 18:09 chinmaygarde

Would it be useful to walk over our tracing workflow to see how and what we can expose via DevTools?

Yes, it definitely would. Feel free to throw something on my calendar.

kenzieschmoll avatar Sep 16 '22 20:09 kenzieschmoll

@jonahwilliams this sounds similar to the issue you were describing to me yesterday wrt latency

kenzieschmoll avatar Mar 02 '23 22:03 kenzieschmoll

Ahh perfect. The idea i had was to track vsync delay and add the to build time. That would surface cases where build finished later due to being delayed by jank between frames, but not cases where there were no frames being pumped.

jonahwilliams avatar Mar 02 '23 22:03 jonahwilliams

Here's a sample where an expensive calculation happens in setState(), which is computed between frames:

sample code
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 30;
  late int _fib;

  @override
  void initState() {
    super.initState();
    _fib = _fibonacci(_counter);
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
      _fib = _fibonacci(_counter + 1);
    });
  }

  int _fibonacci(int n) {
    return switch (n) {
      0 || 1 => n,
      var x => _fibonacci(x - 1) + _fibonacci(x - 2),
    };
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: ConstrainedBox(
        constraints: BoxConstraints.expand(),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          textDirection: TextDirection.ltr,
          children: <Widget>[
            Text(
              'Count:',
              style: Theme.of(context).textTheme.headlineLarge,
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.displayLarge,
            ),
            Text(
              'Fibonacci:',
              style: Theme.of(context).textTheme.headlineLarge,
            ),
            Text(
              '$_fib',
              style: Theme.of(context).textTheme.displayLarge,
            ),
          ],
        ),
      ),
      floatingActionButton: RepaintBoundary(
        child: FloatingActionButton(
          onPressed: _incrementCounter,
          tooltip: 'Increment',
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

johnpryan avatar Aug 02 '23 16:08 johnpryan