Merge pull request #130 from davidmorgan/improve-performance-use-queue

Fix performance issues due to list.removeAt(0), which makes parsing O(n^2).
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98d13e3..7eac77d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.5.3
+
+* Improve performance for large numbers of args.
+
 ## 1.5.2
 
 * Added support for `usageLineLength` in `CommandRunner`
diff --git a/lib/src/allow_anything_parser.dart b/lib/src/allow_anything_parser.dart
index 46f7e12..e6f13d0 100644
--- a/lib/src/allow_anything_parser.dart
+++ b/lib/src/allow_anything_parser.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:collection';
+
 import 'arg_parser.dart';
 import 'arg_results.dart';
 import 'option.dart';
@@ -77,7 +79,7 @@
 
   @override
   ArgResults parse(Iterable<String> args) =>
-      Parser(null, this, args.toList()).parse();
+      Parser(null, this, Queue.of(args)).parse();
 
   @override
   String getUsage() => usage;
diff --git a/lib/src/arg_parser.dart b/lib/src/arg_parser.dart
index eda76ed..72211ba 100644
--- a/lib/src/arg_parser.dart
+++ b/lib/src/arg_parser.dart
@@ -320,7 +320,7 @@
   /// Parses [args], a list of command-line arguments, matches them against the
   /// flags and options defined by this parser, and returns the result.
   ArgResults parse(Iterable<String> args) =>
-      Parser(null, this, args.toList()).parse();
+      Parser(null, this, Queue.of(args)).parse();
 
   /// Generates a string displaying usage information for the defined options.
   ///
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index 8df2535..2a41f17 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -2,6 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:collection';
+
 import 'arg_parser.dart';
 import 'arg_parser_exception.dart';
 import 'arg_results.dart';
@@ -28,7 +30,7 @@
   final ArgParser grammar;
 
   /// The arguments being parsed.
-  final List<String> args;
+  final Queue<String> args;
 
   /// The remaining non-option, non-command arguments.
   final rest = <String>[];
@@ -42,7 +44,7 @@
   }
 
   /// The current argument being parsed.
-  String get current => args[0];
+  String get current => args.first;
 
   /// Parses the arguments. This can only be called once.
   ArgResults parse() {
@@ -58,7 +60,7 @@
     while (args.isNotEmpty) {
       if (current == '--') {
         // Reached the argument terminator, so stop here.
-        args.removeAt(0);
+        args.removeFirst();
         break;
       }
 
@@ -67,7 +69,7 @@
       var command = grammar.commands[current];
       if (command != null) {
         validate(rest.isEmpty, 'Cannot specify arguments before a command.');
-        var commandName = args.removeAt(0);
+        var commandName = args.removeFirst();
         var commandParser = Parser(commandName, command, args, this, rest);
 
         try {
@@ -92,7 +94,7 @@
       // This argument is neither option nor command, so stop parsing unless
       // the [allowTrailingOptions] option is set.
       if (!grammar.allowTrailingOptions) break;
-      rest.add(args.removeAt(0));
+      rest.add(args.removeFirst());
     }
 
     // Invoke the callbacks.
@@ -116,7 +118,7 @@
     validate(args.isNotEmpty, 'Missing argument for "${option.name}".');
 
     setOption(results, option, current);
-    args.removeAt(0);
+    args.removeFirst();
   }
 
   /// Tries to parse the current argument as a "solo" option, which is a single
@@ -136,7 +138,7 @@
       return parent.parseSoloOption();
     }
 
-    args.removeAt(0);
+    args.removeFirst();
 
     if (option.isFlag) {
       setFlag(results, option, true);
@@ -186,7 +188,7 @@
       }
     }
 
-    args.removeAt(0);
+    args.removeFirst();
     return true;
   }
 
@@ -217,7 +219,7 @@
     var name = longOpt[1];
     var option = grammar.options[name];
     if (option != null) {
-      args.removeAt(0);
+      args.removeFirst();
       if (option.isFlag) {
         validate(longOpt[3] == null,
             'Flag option "$name" should not be given a value.');
@@ -240,7 +242,7 @@
         return parent.parseLongOption();
       }
 
-      args.removeAt(0);
+      args.removeFirst();
       validate(option.isFlag, 'Cannot negate non-flag option "$name".');
       validate(option.negatable, 'Cannot negate option "$name".');
 
diff --git a/test/parse_performance_test.dart b/test/parse_performance_test.dart
new file mode 100644
index 0000000..16e412f
--- /dev/null
+++ b/test/parse_performance_test.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:args/args.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('ArgParser.parse()', () {
+    test('is fast', () {
+      var parser = ArgParser()..addFlag('flag');
+
+      var baseSize = 200000;
+      var baseList = List<String>.generate(baseSize, (_) => '--flag');
+
+      var multiplier = 10;
+      var largeList =
+          List<String>.generate(baseSize * multiplier, (_) => '--flag');
+
+      var baseAction = () => parser.parse(baseList);
+      var largeAction = () => parser.parse(largeList);
+
+      // Warm up JIT.
+      baseAction();
+      largeAction();
+
+      var baseTime = _time(baseAction);
+      var largeTime = _time(largeAction);
+
+      print('Parsed $baseSize elements in ${baseTime}ms, '
+          '${baseSize * multiplier} elements in ${largeTime}ms.');
+
+      expect(largeTime, lessThan(baseTime * multiplier * 3),
+          reason:
+              'Comparing large data set time ${largeTime}ms to small data set time '
+              '${baseTime}ms. Data set increased ${multiplier}x, time is allowed to '
+              'increase up to ${multiplier * 3}x, but it increased '
+              '${largeTime ~/ baseTime}x.');
+    });
+  });
+}
+
+int _time(void Function() function) {
+  var stopwatch = Stopwatch()..start();
+  function();
+  return stopwatch.elapsedMilliseconds;
+}