openlang

Marking the filter async did nothing for the freeze

Search filters a list of around forty thousand rows, and the UI freezes for a moment on every query. I figured async was the fix, so the filter became this and the tap handler awaits it.

Future<List<Entry>> filterRows(List<Entry> rows, String query) async {
  final out = <Entry>[];
  for (final r in rows) {
    if (r.matches(query)) out.add(r);
  }
  return out;
}

// in the tap handler
setState(() => loading = true);
final result = await filterRows(allRows, query);

Same freeze. The spinner I turn on right before the call doesn't even show up. I know Isolate.run exists, I've used it for a big JSON decode, but it felt like a lot for a filter, and I thought await was supposed to keep frames going while it waits. What is the await actually doing here?

后续讨论 8

顺着彼此的依据读下去
  1. 固定链接

    I read this the way I'd read a CompletableFuture.supplyAsync call in Java. There the body runs on a pool thread and the caller only waits for the value, so a slow loop inside it never touches the UI thread. If Future works the same way here, the filter itself isn't what freezes you. I'd look at what runs after the await instead. Rebuilding forty thousand rows on the UI thread in one go could easily cost that much, and the spinner may simply be losing the race with that rebuild.

  2. 固定链接

    Nope, and it's an easy map to make coming from Java. A Dart Future is a value that arrives later, not a thread. The whole app runs in one isolate, and that isolate has one thread running one event loop. On top of that, an async function runs synchronously until its first await. filterRows has no await inside, so the entire loop runs right there in the tap handler, before the Future is even returned. The frame that would draw the spinner sits in the event queue behind it. The rebuild after the await isn't the slow part, it doesn't even start until the loop is done.

    Moving the work off that loop takes a second isolate.

    import 'dart:isolate';
    
    List<Entry> filterRows(List<Entry> rows, String query) =>
        rows.where((r) => r.matches(query)).toList();
    
    // in the tap handler
    final result = await Isolate.run(() => filterRows(rows, query));

    Now the await waits for a message from the other isolate, and the event loop is free in between. I do wish the keyword sounded less like it moves work somewhere else.

  3. 固定链接

    You're right, and I had it backwards. I carried the pool thread over from supplyAsync, and that's exactly the part Dart never promised. Reading n.vos's code again with that fixed, the loop runs inside the call itself, and the rebuild I blamed hadn't even started when the screen froze.

  4. 固定链接

    It depends on what is awaited. Follow the frame through the queues. An await on a future that has already completed resumes in a microtask. Every microtask runs before the next event. The frame is an event, so it still waits behind the whole loop. await Future.delayed(Duration.zero) resumes from a timer, and a timer is an event. Now the loop goes to the back of the event queue, and a frame that was already waiting gets its turn first. So the chunked version keeps the screen drawing. Each chunk still runs on the UI isolate, though, and a chunk that is too long drops frames just the same.

  5. 固定链接

    That worked, after one stumble. In my handler allRows is a field, and the first try used it straight inside the closure. That captures this and drags the whole State along into the isolate, the same trap as my Loader. Copying it into a local before Isolate.run fixed it, and now the spinner shows and the list scrolls while the filter runs.

  6. 固定链接

    The synchronous start is younger than it looks. Early Dart did it the other way round, and an async function handed back its Future first and started its body only later. Dart 2 changed that so the body runs synchronously up to the first await (listed as a breaking change, and fairly, since some code had leaned on the body starting later without anyone writing it down, and the new rule matched what JavaScript async functions had done from their first day). Under either rule n.vos's loop runs on the one isolate, so the freeze would have been there all along.

  7. 固定链接

    Isolate.run is the right default here, with one narrowing. Whatever the closure captures is sent to the new isolate, so the forty thousand rows travel over as a copy on every query. The result comes back through Isolate.exit, which the docs say sends it without copying, so only the input side pays. That cost grows with the list, and it's worth timing a search once with the real data before calling the freeze solved.

同一个问题的其他角度

  1. Workers stuck on the results send after the context is canceled

    Ran into a simple thing and spent longer on it than I'd like. A pool of eight workers reads tasks from jobs and writes result…

    回复 8最近活动 go
  2. Eight queue slots can still leave 88 goroutines waiting

    A buffered channel bounds the jobs sitting in it. It does nothing about the goroutines blocked trying to send. Say a server g…

    回复 8最近活动 go
  3. asyncio.TaskGroup when one fetch fails

    Concurrency comparisons between languages usually start with syntax. I'd start with what the calculation needs when something…

    回复 15最近活动 pythonjuliago