Flutter debugger call stack bug?

I am new to flutter and to Dart. Reading documentation, playing with samples, tutorials and all that. Coming from a C/C++, perl python java etc background, the program flow seems odd. This is probably due to Dart’s emphasis on parallelism and asynchronicity without doing it “by hand” as in the languages I’m familiar with. Just starting from the basics, sitting on a breakpoint at main:

main

(to be continued replying to myself, new users limited to one embedded image)

Part 2: I would expect stepping in to lead to the MyApp constructor, back out, then into runApp and never return. The first part, yeah… Step in, and:

MyApp

Part 3: But there’s no sign of calling the superclass constructor. Seems like StatelessWidget constructor invocation is inlined or something.

Edit: nope, once I set a breakpoint in runApp I was prompted to debug all code, now I can step into the framework / library functions.

Also, super.key is null, which seems weird. Step in bounces back to that single line main. Step in again, main returns. Seems like main isn’t main in the classical sense, more like an init function.

Edit: indeed, looks like _startMicrotaskLoop is where the real main loop resides, and that happens after main has completed as an init function. The breakpoint I had set in MyApp’s build function is hit under there. It’s starting to make sense…

Part 4: Seems like the build function is a deferred constructor of sorts. Which is a common pattern, no big deal. What really throws me off though is the call stack, trying to understand how we got there… The call stack drops down into a repeating cycle. If those numbers in the right column are timestamps that’s an interesting feature, but they also confirm that the call stack is doubling back on itself. Is the debugger’s stack unwind bugging out, or is this something I have simply not come to understand yet about the way Dart does parallelism and asynchronicity?

Like are Dart call stacks not based on the CPU stack pointer register, but some sort of linked list which can become circularized by design?

1 Like

due to Dart’s emphasis on parallelism and asynchronicity

There is no parallelism in Dart (it’s a single-thread event loop language). And “asynchronicity” is just a callback function, like any other modern-ish language. There is nothing unusual here.

I would expect stepping in to lead to the MyApp constructor

Odd, I know, but constructors are not used in Dart. They exist, but they are so useless that nobody uses them. For instance: one of the top Dart features IMO is const objects. Constructor code are not allowed in const objects, so, since your objects, especially on Flutter, will be almost all const, constructors are a no-no. So, yes, all Flutter constructors are empty, so there is no step in into those.

Also, super.key is null, which seems weird.

Not every widget needs a key, so, you pass a non existing value to it. Hence, hull. It’s an optional parameter. Required parameters are marked with required.

Seems like main isn’t main in the classical sense

Classical for what? You are writting in Dart! Classical for X or Y doesn’t make any sense. Don’t try to compare oranges with your pineapple.

more like an init function.

Yes. Flutter is an Android/iOS/Windows/Linux/MacOS native piece of code, that have a Skia/Impeller canvas and, when initialized by its host application, call the main method. Flutter doesn’t run natively on any platform (it’s kinda like a custom component for each platform, controlled by native code). So, yes, inside Dart, you are in Dart world, so main is not native at all. There is always a runtime running (even when compiled as native binary). I think only low-ish level languages (C, C++, etc.) can run natively (so the main function is actually a piece of Assembly code running). Every other language out there have a runtime (meaning: the “classical” main is the runtime entry-point that delegates to your code. JVM, .net JIT, QBasic, etc.)

Seems like the build function is a deferred constructor of sorts.

Nop. build is an update method of a game engine (almost literally). It runs when your widget changes. Your widget is a state (it does not render anything, it’s just a value holder, a blueprint). Whenever that info has to be built, it runs. It is called by Flutter runtime.

call stack drops down into a repeating cycle

Again: Dart is an event loop language. It runs in an infinite loop. Flutter is, basically, a game engine (read this to be familiar how game engine works: What is the Game Loop | MonoGame). All “game” data (for instance, a position of a ship in the screen) are hold in pure classes (they just hold values, nothing more). As fast as the engine can, it will call the update method (so you can check if the user is pressing -> and then add +1 to the ship x position). 60x a second (or whatever is your monitor refresh rate), the draw method is called: it takes all your classes and stamp them on the screen (basically, a photo of your current state). This is how any game engine works, and this is how Flutter works, kinda of…

What is important is: your widgets are only representation of your state (for example: Text("Hello world") is a String holder that holds your fixed state “Hello world”, and that’s it. That’s all Text does. In the background, (almost) without your control, a render loop find your “String holder” and then render the text to a surface and so on…

Probably a shit explanation, but…

Probably a shit explanation, but…

Actually no. That was very helpful and informative, thank you.

There is no parallelism in Dart (it’s a single-thread event loop language). And “asynchronicity” is just a callback function, like any other modern-ish language. There is nothing unusual here.

Oh, interesting. I ass-umed that the emphasis on asynchronicity would go hand in hand with parallelism. Chocolate and peanut butter and all that…

Seems like main isn’t main in the classical sense

Classical for what? You are writting in Dart! Classical for X or Y doesn’t make any sense. Don’t try to compare oranges with your pineapple.

Classical for C/C++, because that’s my primary frame of reference from which I am attempting to learn Dart / flutter. Consider me chastised.

Flutter doesn’t run natively on any platform (it’s kinda like a custom component for each platform, controlled by native code).

Oh, interesting. I also ass-umed that Dart compiles to native code. Which would make hot reload rather difficult, so this makes perfect sense.

Seems like the build function is a deferred constructor of sorts.

Nop. build is an update method of a game engine (almost literally). It runs when your widget changes. Your widget is a state (it does not render anything, it’s just a value holder, a blueprint). Whenever that info has to be built, it runs. It is called by Flutter runtime.

Also makes perfect sense, in light of your clarification that constructors are not used in Dart.

call stack drops down into a repeating cycle

Again: Dart is an event loop language. It runs in an infinite loop.

This doesn’t actually explain why the call stack cycles back on itself and repeats infinitely, as far as I looked (several hundred stack frames). Even an interpreted language has a stack. So I still don’t understand this part. The only thing I can think of is that I was stepping, it was recursively entering a subroutine in which it tries to service pending events, so my stepping in the debugger while trying to study the behavior caused this. Not a perfect analogy, but like in firmware debugging when you hit a breakpoint without interrupts locked, try to step and end up in an ISR. Kinda like the Heisenberg uncertainty principle observer effect. Also not a perfect analogy, but it’ll do.

Again, thank you. Not a shit explanation, this helped a lot.

Oh, interesting. I also ass-umed that Dart compiles to native code.

Yes, it does, when built in release mode.

Notice that native code is one thing. Running it is another.

Comparing Dart with C: in C, you need to include stdio.h to use standard I/O, right? All languages, aside from low-level ones, needs also some kind of support for doing things, such as I/O. Usually, languages have what is called runtime: a piece of code that is embedded in every binary that allows your Dart code (which IS native) to actually call the OS native calls to, let’s say, open a file. ALL languages that are higher level than C has a runtime. So, yes, Dart IS compiled to machine language code, but you need a bit of abstraction to make this code work in the target platform (the runtime).

Which would make hot reload rather difficult

When running Flutter in debug mode, it is actually JITted (there is a compiler inside the debug client that will get Dart code and compile it to run on demand - that’s what Just In Time means. Notice that Apple doesn’t allow that (and it’s kinda slow, especially on mobile devices), so Dart compiles full AOT in release time, to actual X64 and ARM code.

This doesn’t actually explain why the call stack cycles back on itself and repeats infinitely, as far as I looked

Isn’t that the explanation of an infinite loop?

Imagine this: you are animating the color of a text from green to red. That animation will have a total duration of 2 seconds. Imagine you are on a phone with a 120Hz display. Your build method (should) will be called 120 x 2 times (120 calls a second, for 2 seconds). In each call your build method will update the text color. So that stack trace makes sense now, right? You are doing something that is triggering thousands of build calls. That’s the whole normal Flutter stack trace (it’s an infinite loop).

Even an interpreted language has a stack

As does Dart. But Flutter makes kinda useless to track, since it is very verbose.

I don’t know exactly what your intention is with the stack trace, if you could say what you want, I maybe can explain it better. If it is only for understanding, then understand this: Dart is a language (that have a normal stack trace, as you are used to). Flutter is a SDK, a Framework that does (heavy) stuff, so its stack trace is insane. Is exactly the same thing to analyse the stack of a running Doom game =P

in which it tries to service pending events,

It is exactly this. All event loop languages are this: an infinite loop that triggers and consumes events. If you are familiar with, Windows also works this way. Read this: Event Loop — Flutter. Dart uses an event-driven programming… | by Gaurav Swarankar | Medium You can add things to that event loop using SchedulerBinder, scheduleMicrotask, etc. async, in this case is: let someone else do this stuff and let continue the loop, whenever that response arrive, insert in the event so, eventually, the loop will get back to it. It’s NOT like, let’s say, C# Task (that can be either a continuation I/O, like I described, or a real thread… There is no such thing as threads in Dart).

so my stepping in the debugger while trying to study the behavior caused this

It depends how you are stepping. Sometimes, yes, you enter the Flutter framework and it is impossible to get out of there =( I come from a .net world where the debugger is perfect and I can trace the whole stack the way you probably are used to. In Dart this is also true, but Flutter gets in the way =\

when you hit a breakpoint without interrupts locked

Wonderful! Then you’ll understand this. Sometimes, Flutter acts like this: an interrupt that fucks up your debug =) Sometime you will hit the main loop and you will never be able to get out of there using the step in debug function =) The point is: you should NOT be there. Something you did make you get there (maybe you trace too deep or you are in a point where an exception occurred inside Flutter itself and you’ll get back there). But it’s rare. I often don’t have trouble with the step in while stepping in my code only. Flutter/Dart/VSCode plugin really should have an option to NOT step in non-user code (as C# has) =\

EDIT: To be clear: there are TWO infinite loops here: one is the Dart event loop (that article), other is the Flutter loop that keeps triggering widgets build methods, renderers render methods, etc. The screenshot you posted is the second one: the Flutter loop is building the MyApp, which will trigger ALL build methods for ALL children widgets. And a lot of things will retrigger this: hot restart, a widget changing something, a theme change, etc. So, don’t mess with the build method. That method is meant only for updating your widget state and nothing else (remember: in an animation, all those hundreds of build methods will run 120 times a second!).

Notice that Apple doesn’t allow that (and it’s kinda slow, especially on mobile devices), so Dart compiles full AOT in release time, to actual X64 and ARM code.

I haven’t tried hot reload in the iOS simulator yet, just MacOS. I can’t even imagine how to do hot reload in native machine code since the function addresses will change, stuff done for optimization will change. Very interested now to see what happens. I don’t expect it to work. :grinning_face: Perhaps I am in for a pleasant surprise.

This doesn’t actually explain why the call stack cycles back on itself and repeats infinitely, as far as I looked

Isn’t that the explanation of an infinite loop?

Not really. An loop, infinite or bounded, executes within the same stack frame. So no matter how many times the loop has executed, the stack doesn’t grow with iteration count. At least not in what I’m familiar with, and trying now to understand as pertains to Dart / flutter. A call stack (at least as I am familiar with it) is who called who. Bottom to top, not a record of how many times were looped within a particular stack frame. That’s more of a logging thing when debugging. It seems that my notion of a call stack is less applicable to Dart / flutter. Perhaps a Dart call stack is more like a log than what I am familiar with as to what a call stack per se is.

I don’t know exactly what your intention is with the stack trace

Trying to wrap my head around program flow in Dart, studying its execution. I am trying to learn a new language, and while much of it is very familiar, some of it is not. This is case in point.

It is exactly this. All event loop languages are this: an infinite loop that triggers and consumes events.

Yes, exactly! Where things get nasty is when an event loop is processing an event, calls a function to handle it, but somewhere down the call chain, rather than finishing its job and getting back up to the top level event loop to handle the next event, it dives into handling the next event, recursively, before the prior event is even done. I’ve seen that kind of shit before, and…withholding further comment, you get the idea. Not. A. Fan.

The insane call stack I cited from my exploration seems to suggest recursive event loop processing before the current top level event is done. I hope that’s not the case, I hope it’s something else, perhaps with the “call stack” being more of a log of past loop iterations or the debugger going haywire and doubling back in itself during stack unwind.

The point is: you should NOT be there. Something you did make you get there (maybe you trace too deep or you are in a point where an exception occurred inside Flutter itself and you’ll get back there).

Again, yes, exactly! That’s what I was wondering… If because I hit a breakpoint at something I wanted to study, and started stepping, meanwhile the calls I stepped over got down into some function where it was like “oh shit, unprocessed events, better handle these here and now” rather than leaving them be until execution gets back up to the top level event handling loop. Maybe that’s normal with Dart, and maybe it handles that gracefully. Not gonna judge. Or at least try not to. :grinning_face: Just trying to understand.

I can’t even imagine how to do hot reload in native machine code

Hell on earth, basically. And still forbidden by Apple (and this is a good thing).

That’s why Kotlin Multi Platform and Swift don’t do this quite well (even they are JITted languages - at least Kotlin).

So no matter how many times the loop has executed, the stack doesn’t grow with iteration count.

Only if you use tail call optimization. A non-optimized loop (or, should I say, a non “compiler rewriting my bad code” loop will look like this). When I say “loop”, I don’t mean for, while, etc.

A call stack

A call stack is only a return address when using a CALL. A RET will pop the address from the stack and execute a JUMP. But, a loop isn’t that. There is no CALL in loops, only a final JUMP that goto the first address of the loop. This will generate a stack trace like what you are seeing (I’m guessing here, I didn’t actually read the dart/flutter runtime code). There are people in this forum far more competent than me in this matter (including at least one Dart maintainer).

were looped within a particular stack frame

This is not what is happening here, I guess. Remember I said the MaterialApp build method will trigger ALL widgets build methods? This is a recursion call for hundreds of widgets, so the stack trace makes sense (and since all Widgets inherits from the same class, all calls will look the same). So, contrary to what I said above, this is a simple and true stack call trace (but a recursive one).

Remember: there are (at least) 3 trees in Flutter that are recursive: the Widgets (the blueprints/state), the Element (I won’t even try to explain that =P) and the Renderer (which will get a snapshot of the current state and then actually render the widgets’ content). There are two good video series about this, starting here: https://www.youtube.com/watch?v=Beiu8IGbStc&list=PLjxrf2q8roU0WrDTm4tUB430Mja7dQEVP and https://www.youtube.com/watch?v=Y2aBMjWVv2Y&list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl)

Perhaps a Dart call stack is more like a log than what I am familiar with as to what a call stack per se is

It isn’t. You are mistaken Flutter for Dart and vice-versa. What you are seeing in that screenshot is a (huge) recursive call started by your main method that calls runApp, that calls your MyApp build method and that goes on for every and each widget bellow it. When it finishes that, all start again when something changes (shortcutted to the changing widget and bellow).

it dives into handling the next event, recursively, before the prior event is even done

That’s what async is in all languages (some, like C# have extras, but all of them are exactly that: a method that is abandoned because someone else will be busy handling something that is not our code and eventually the response will appear (either by calling a callback or by adding a response event in the event loop).

Not. A. Fan.

Because you are in the wrong place =) What you are doing is: you are debugging a C code, line by line and, somehow, you end up in the operating system event loop. It would not be nice at all (you would be interrupted by every single OS event, like mouse move, thousands of times per second).

I still don’t understand why are you doing this.

current top level event is done

It could be. I don’t know this, but imagine the top event is something like the application state (if it is in background, etc.). That will never go away (and will never complete until the app shuts down). Remember: you are not dealing with a procedural simple app, like a C cli. You are dealing with a huge multi-language, multi-stack framework insanely complex.

If because I hit a breakpoint at something I wanted to study

The moment you enter non-your-code, you’ll start to debug a very complicated multi-facet application. Definitely not the best way to learn it.

What you are doing is, basically, in a first-date with a pretty woman and cutting her intestines to see how she works internally (a.k.a. know her). Isn’t going to work.

Or, another example: trying to debug a operating system, from your user-code to the kernel. You’ll learn only how to be insane =P

To elaborate further, you see that a lot in windows. The top-level event loop is calling GetMessage. Handling a message, if you need user response (yes/no, file select dialog, whatever) and create a modal dialog, more often than not, rather than modality blocking other user input but still handling the user response back up in the top-level event loop and removing modality restrictions when done, the modal dialog implementation will recursively enter an event loop calling GetMessage, handling events that ought to be handled at the top-level, even though up the stack you have an event that is in process but not done yet. It’s a shitty design, but the way Windows modal dialog boxes work, it forces you into it.

For Windows? Yes.

For a game engine/language/reactive declarative UI framework? No.

Flutter is built like a game engine (that loop of update state → render → restart), and that is a good thing. The fastest piece of software out there are games (which have to render an insane amount of data in less than 7ms (in my machine, with 144Hz)).

It’s different from React Native, for instance, where you have a constant communication between JavaScript and the native components (and still both JavaScript and the native part are event loops as well). In the very end, inside Android’s/iPhone inner guts, they are all game engines, getting some states, rendering textures, shaders, etc. and sending to the GPU. In the end, all things are the same. Flutter just gives you a hell more of control over it because it does that (the Android/IOS are just a blank canvas, they don’t do anything, UI related, exceptions aside). So all that complexity you don’t see because you don’t debug your OS you are seeing now.

Only if you use tail call optimization

I beg to differ. Tail call optimization pops the stack frame entering the callee, obscuring the caller. It doesn’t exponentiate the caller, which is what I observed. Not to bust your chops, but I’ve been a professional software engineer for 28 years, hobbyist for 17 years before that, so I do know this stuff. Haha perhaps this is a case of “can’t teach an old dog new tricks”. :grinning_face:

This will generate a stack trace like what you are seeing

Only if the call stack shown by the flutter debugger is more of a log than a stack unwind. Which it may well be. Seeming that way, and that’s what I’m trying to clear up here.

This is a recursion call for hundreds of widgets

Not in this case. I’m a noob. I’m studying Dart / Flutter with the provided sample / tutorial (Write your first app | Flutter), which as far as I have gotten yet, has 3 widgets.

There are two good video series about this, starting here: https://www.youtube.com/watch?v=Beiu8IGbStc&list=PLjxrf2q8roU0WrDTm4tUB430Mja7dQEVP and https://www.youtube.com/watch?v=Y2aBMjWVv2Y&list=PLjxrf2q8roU1fRV40Ec8200rX6OuQkmnl)

Thank you, I will watch those.

What you are seeing in that screenshot is a (huge) recursive call started by your main method that calls runApp, that calls your MyApp build method and that goes on for every and each widget below it.

Pretty much as I figured, but as I said, the app as I do my studying to learn has 3 widgets thus far. When I kept hitting “show more stack frames” it went on and on and on and on. Far more than this can explain. Plus it seems like flutter debugger call stacks have time stamps, if I’m correctly interpreting the blocked numbers on the far right. Cool feature, but those looped too. I truly believe that the stack unwind as shown by the debugger doubled back on itself. If so, that would be something for the devs to look at, if they care to.

That’s what async is in all languages

Did you mean “await”? Because that is explicit. I’ma wait right here until the thing I’m waiting for is done.

I still don’t understand why are you doing this.

Because I have an idea for a mobile app, cross platform android and iOS, and flutter seems like the best solution. Flutter depends on Dart. Thus I need to learn Flutter and Dart. I could just copy/paste go for it, but I prefer to actually understand what I’m doing. So I took the deep dive, came up with some questions, and you had the misfortune of indulging me. :wink:

current top level event is done

It could be. I don’t know this, but

Until execution of the whole call tree completes and gets back to the event loop, the event in process isn’t done. By way of analogy, what I complained about windows modal dialogs and GetMessage (loop) → HandleMyMessage → DoSomeStuff → modal dialog → GetMessage (loop), that inner loop pulls the rug out from under HandleMyMessage and DoSomeStuff. Whatever those functions were looking at becomes invalid by the time the modal dialog is done. One must be VERY cognizant of such things. Hence “why are you doing this”.

The moment you enter non-your-code, you’ll start to debug a very complicated multi-facet application.

Funny, we have the opposite take on this. Until I discovered the option for the debugger to enter not-my-code, things I was trying to study were being hidden.

I should just ignore your first date comparison, but I can’t resist. It’s not taking her home and cutting out her liver to go with a nice glass of chianti, it’s getting to know her, her background, her goals, her ambitions. Asking her about herself.

I beg to differ. Tail call optimization pops the stack frame entering the callee, obscuring the caller.

Not in all languages. This can be true in the things you undoubtedly know, but it is not true in things you don’t know. I know, for a fact, that C# tail optimization are often replaced with a goto. There is only one entry and the end of recursion just jumps to the start of the method. So, in this case, even an infinite recursive function doesn’t stack overflow, because there is only one push to the stack.

Different languages have different solutions for the same problem.

professional software engineer for 28 years

Same here. I started with 1986, but I can’t tell shit about C++ (and I guess most of what I said about Dart, a language that I use every day since 2019, it’s just plain wrong) =P It is impossible to know everything. And even if we dominate the basics, there is always a Microsoft engineer that finds some odd stupid way to write things that doesn’t make any sense =P

Only if the call stack shown by the flutter debugger is more of a log than a stack unwind.

It is not. The example you show in your screenshot is definitely a recursive build triggered by app initialization. It would be just the same if it was written in C++/C# with a recursive call for lots of items that inherits the same class and are calling the same methods.

which as far as I have gotten yet, has 3 widgets.

But how many times they are being build? And what is the source (trigger) of that? I’m not the best person to answer that, but it is not abnormal or bugged in any way, that I know for sure.

Please, read/watch about the Flutter three trees and you’ll see what you learn in this stack trace =)

When I kept hitting “show more stack frames” it went on and on and on and on.

Maybe it’s that update -> render -> restart game loop I mentioned? This must run as often as possible, in a loop. That’s how these engines handle inputs. What does NOT happen though is the rendering: since there is nothing new, nothing new is rendered (you can see that in the Dev Tools, where the frame graph will just stop when there is nothing going on in the screen). The What is the Game Loop | MonoGame is exactly what this is about (of course, it is not the same, as Flutter is a bit more complicated than XNA/MonoGame, but it is the same principle).

The stack you posted shows a continuous rendering triggered by context rebuilds (Element.rebuild). You are inside an animation or something is asking your widgets to rebuild. const Widget() is not optional! When a class have const constructors, be sure to use that, otherwise, Flutter will trigger the rebuild of it and its child without need.

If you can post your code, I could tell more about what is happening.

1 Like

I beg to differ. Tail call optimization pops the stack frame entering the callee, obscuring the caller.

Not in all languages. This can be true in the things you undoubtedly know, but it is not true in things you don’t know. I know, for a fact, that C# tail optimization are often replaced with a goto.

Interesting. My experience with tail call optimization is mostly with C compiled to ARM assembly. Tail call optimization there is popping the caller’s stack frame off, then B.W to the callee. The caller basically disappears. So even though the callers were A → B → C, if B has tail call optimization, you end up seeing A → C. Which makes perfect sense, minimize stack usage. In what you describe, you’d have stale stack frames. Which in turn matches what I saw in the Dart / flutter call stack that I cited.

Please, read/watch about the Flutter three trees and you’ll see what you learn in this stack trace

I definitely will, just not right now. I am making dinner and have some friends coming over in a bit.

If you can post your code, I could tell more about what is happening.

It is literally just the codelab from flutter tutorials. It starts from the Application template in flutter: new project.

I got as far as the part about add a button. Then when I tried running it in iOS and android simulators, I noticed that the app’s rendering was tucked under the virtual phone’s system stuff at the top, battery indicator, etc, so I added a SizedBox. SafeArea would have been a better choice (SizedBox was just a stopgap measure). Nothing consequential. But I was trying to understand it at a deeper level, and seeing some stuff I didn’t understand, which was when I went into my deep dive and came up…here. By the way, thank you very much for your advice and help. Much appreciated.

1 Like

@sdbtietz Just to summarize answers to your original questions:

  • Dart stack is just like a stack in any other programming language. If we are talking about native implementation of Dart then call stack will reside on the native stack of a thread in the form of frames, just like a call stack of a C++ program.
  • There is no looping in the call-stack you have shared, it is just a deep recursive call like @evaluator118 has pointed out.
  • Numbers on the right hand side are source locations (i.e. line:column), not timestamps.
  • Dart does not support nested event loops unlike Windows. Because nested event loops are evil. You always process one event to completion before you start on the next one.
  • Native implementation of Dart always compiles source to native code before it runs (at runtime for debug build susing a two-tier JIT compiler or ahead of time for release builds)

Did I miss something?

2 Likes