Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DocumentFile.findFile causes ANR #144

Open
clragon opened this issue Aug 20, 2023 · 2 comments
Open

DocumentFile.findFile causes ANR #144

clragon opened this issue Aug 20, 2023 · 2 comments
Labels
bug Something isn't working

Comments

@clragon
Copy link

clragon commented Aug 20, 2023

Describe the bug

Using DocumentFile.findFile inside of a folder with many files will somehow use so much CPU that it hangs the main UI thread.

To Reproduce

  • Compile, install and run the sample code below on an Android device
  • Select a folder with many images
  • Trigger the ANR
  • Attempt to click increment the counter
  • Observe that the increment button cannot be clicked because the App no longer responds to user input

Note that the sample code actually uses DocumentFile.child but this triggers an unreasonably costly findFile regardless.

Sample code
import 'package:flutter/material.dart';
import 'package:shared_storage/shared_storage.dart';

void main() => runApp(const App());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          brightness: Brightness.dark,
          seedColor: Colors.deepPurple,
        ),
        useMaterial3: true,
      ),
      home: const Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({super.key});

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int count = 0;
  DocumentFile? folder;
  bool busy = false;
  bool hangs = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('findFile ANR'),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              '$count',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(width: 32),
            ElevatedButton(
              onPressed: () => setState(() => count++),
              child: const Text('increment'),
            ),
            const SizedBox(width: 32),
            AnimatedOpacity(
              duration: const Duration(milliseconds: 200),
              opacity: hangs ? 1 : 0,
              child: Text(
                'try incrementing now',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ),
            const SizedBox(width: 32),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                ElevatedButton(
                  onPressed: !busy
                      ? () async {
                          setState(() => busy = true);
                          folder = await (await openDocumentTree())
                              ?.toDocumentFile();
                          setState(() => busy = false);
                        }
                      : null,
                  child: const Text('select'),
                ),
                const SizedBox(width: 16),
                ElevatedButton(
                  onPressed: !busy && folder != null
                      ? () async {
                          setState(() {
                            busy = true;
                            hangs = true;
                          });
                          await folder!.child('any.png');
                          setState(() {
                            busy = false;
                            hangs = false;
                          });
                        }
                      : null,
                  child: const Text('trigger ANR'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Expected behavior

Using DocumentFile.findFile should not hang the UI thread.

Screenshots

Here is a video demonstration of the issue:

MNML-August20-083835PM.mp4

Smartphone:

  • Device: Samsung Galaxy S20 FE
  • OS: Android 13
  • Flutter 3.13.0, shared_storage 0.8.0
@clragon clragon added the bug Something isn't working label Aug 20, 2023
@clragon
Copy link
Author

clragon commented Aug 20, 2023

Further, I have found out that I can work around this issue with the following code:

await Uri.parse('${folder.uri}%2F${Uri.encodeComponent(fileName)}')).toDocumentFile();

which is basically just manually constructing the same URI and then converting it into a DocumentFile.
For whatever reason, this does not trigger a findFile call and therefore doesnt run into the ANR issue.

However, I would obviously strongly prefer constructing files with the intended APIs and not manually.

@alexrintt
Copy link
Owner

Hi! I could reproduce here as well, thank you very much for the detailed issue.

findFiles is flawed by design, it's a brute force search implemented by the android sdk (a simple linear search) which is a performance disaster when we have a folder with several files (which is the case for the vast majority of the modern devices).


About your workaround, it won't work with some edgy content providers because sometimes the URI doesn't follow the
parent/child.ext naming pattern but a provider_storage_id/document_id pattern.

I'll try to fix it using content providers and manually query the storage DB, but in the worst scenario we can just run the findFiles method in a background thread (it will still be slow but will fix the main thread blocking).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants