Skip to content

Commit 06ad7ce

Browse files
authored
Refactor temporary file handling for efficiency (#1865)
1 parent b7b0eb0 commit 06ad7ce

File tree

6 files changed

+73
-66
lines changed

6 files changed

+73
-66
lines changed

bin/dartdoc.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:args/args.dart';
1111
import 'package:dartdoc/dartdoc.dart';
1212
import 'package:dartdoc/src/html/html_generator.dart';
1313
import 'package:dartdoc/src/logging.dart';
14+
import 'package:dartdoc/src/tool_runner.dart';
1415
import 'package:stack_trace/stack_trace.dart';
1516

1617
class DartdocProgramOptionContext extends DartdocGeneratorOptionContext
@@ -94,8 +95,9 @@ void main(List<String> arguments) async {
9495
}
9596
}, when: config.asyncStackTraces);
9697
} finally {
97-
// Clear out any cached tool snapshots.
98+
// Clear out any cached tool snapshots and temporary directories.
9899
SnapshotCache.instance.dispose();
100+
ToolTempFileTracker.instance.dispose();
99101
}
100102
}
101103

lib/src/dartdoc_options.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:analyzer/dart/element/element.dart';
2121
import 'package:args/args.dart';
2222
import 'package:dartdoc/dartdoc.dart';
2323
import 'package:dartdoc/src/io_utils.dart';
24+
import 'package:dartdoc/src/tool_runner.dart';
2425
import 'package:dartdoc/src/tuple.dart';
2526
import 'package:path/path.dart' as pathLib;
2627
import 'package:yaml/yaml.dart';
@@ -243,11 +244,12 @@ class SnapshotCache {
243244
return snapshots[toolPath];
244245
}
245246

246-
void dispose() {
247+
Future<void> dispose() {
248+
_instance = null;
247249
if (snapshotCache != null && snapshotCache.existsSync()) {
248-
snapshotCache.deleteSync(recursive: true);
250+
return snapshotCache.delete(recursive: true);
249251
}
250-
_instance = null;
252+
return null;
251253
}
252254
}
253255

@@ -288,12 +290,16 @@ class DartToolDefinition extends ToolDefinition {
288290
class ToolConfiguration {
289291
final Map<String, ToolDefinition> tools;
290292

293+
ToolRunner _runner;
294+
ToolRunner get runner => _runner ??= ToolRunner(this);
295+
291296
ToolConfiguration._(this.tools);
292297

293298
static ToolConfiguration get empty {
294299
return new ToolConfiguration._({});
295300
}
296301

302+
// TODO(jcollins-g): consider caching these.
297303
static ToolConfiguration fromYamlMap(
298304
YamlMap yamlMap, pathLib.Context pathContext) {
299305
var newToolDefinitions = <String, ToolDefinition>{};

lib/src/markdown_processor.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import 'package:dartdoc/src/tuple.dart';
1515
import 'package:dartdoc/src/warnings.dart';
1616
import 'package:html/parser.dart' show parse;
1717
import 'package:markdown/markdown.dart' as md;
18-
import 'package:quiver/iterables.dart' as quiverIterables;
1918

2019
const validHtmlTags = const [
2120
"a",

lib/src/model.dart

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ import 'package:dartdoc/src/markdown_processor.dart' show Documentation;
5757
import 'package:dartdoc/src/model_utils.dart';
5858
import 'package:dartdoc/src/package_meta.dart' show PackageMeta, FileContents;
5959
import 'package:dartdoc/src/special_elements.dart';
60-
import 'package:dartdoc/src/tool_runner.dart';
6160
import 'package:dartdoc/src/tuple.dart';
6261
import 'package:dartdoc/src/utils.dart';
6362
import 'package:dartdoc/src/warnings.dart';
@@ -4121,9 +4120,6 @@ abstract class ModelElement extends Canonicalization
41214120
/// 2018-09-18T21:15+00:00
41224121
Future<String> _evaluateTools(String rawDocs) async {
41234122
if (config.allowTools) {
4124-
var runner = new ToolRunner(config.tools, (String message) async {
4125-
warn(PackageWarning.toolError, message: message);
4126-
});
41274123
int invocationIndex = 0;
41284124
return await _replaceAllMappedAsync(rawDocs, basicToolRegExp,
41294125
(basicMatch) async {
@@ -4138,7 +4134,8 @@ abstract class ModelElement extends Canonicalization
41384134
// Count the number of invocations of tools in this dartdoc block,
41394135
// so that tools can differentiate different blocks from each other.
41404136
invocationIndex++;
4141-
return await runner.run(args,
4137+
return await config.tools.runner.run(args,
4138+
(String message) async => warn(PackageWarning.toolError, message: message),
41424139
content: basicMatch[2],
41434140
environment: {
41444141
'SOURCE_LINE': lineAndColumn?.item1?.toString(),
@@ -4153,7 +4150,7 @@ abstract class ModelElement extends Canonicalization
41534150
'ELEMENT_NAME': fullyQualifiedNameWithoutLibrary,
41544151
'INVOCATION_INDEX': invocationIndex.toString(),
41554152
}..removeWhere((key, value) => value == null));
4156-
}).whenComplete(runner.dispose);
4153+
});
41574154
} else {
41584155
return rawDocs;
41594156
}
@@ -4783,7 +4780,7 @@ class PackageGraph {
47834780
// specialClasses handler so when we find them, they get added.
47844781
specialClasses = new SpecialClasses();
47854782
// Go through docs of every ModelElement in package to pre-build the macros
4786-
// index.
4783+
// index. Uses toList() in order to get all the precaching on the stack.
47874784
List<Future> precacheFutures = precacheLocalDocs().toList();
47884785
for (Future f in precacheFutures) await f;
47894786
_localDocumentationBuilt = true;

lib/src/tool_runner.dart

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -19,60 +19,49 @@ typedef FakeResultCallback = String Function(String tool,
1919
/// limiting both parallelization and the number of open temporary files.
2020
final MultiFutureTracker _toolTracker = new MultiFutureTracker(4);
2121

22-
/// A helper class for running external tools.
23-
class ToolRunner {
24-
/// Creates a new ToolRunner.
25-
///
26-
/// Takes a [toolConfiguration] that describes all of the available tools.
27-
/// An optional `errorCallback` will be called for each error message
28-
/// generated by the tool.
29-
ToolRunner(this.toolConfiguration, [this._errorCallback]);
22+
/// Can be called when the ToolRunner is no longer needed.
23+
///
24+
/// This will remove any temporary files created by the tool runner.
25+
class ToolTempFileTracker {
26+
final Directory temporaryDirectory;
3027

31-
final ToolConfiguration toolConfiguration;
32-
final ToolErrorCallback _errorCallback;
33-
int _temporaryFileCount = 0;
28+
ToolTempFileTracker._() : temporaryDirectory = Directory.systemTemp.createTempSync('dartdoc_tools_');
3429

35-
Future<Directory> _temporaryDirectory;
36-
Future<Directory> get temporaryDirectory {
37-
if (_temporaryDirectory == null) {
38-
_temporaryDirectory = Directory.systemTemp.createTemp('dartdoc_tools_');
39-
}
40-
return _temporaryDirectory;
41-
}
30+
static ToolTempFileTracker _instance;
31+
static ToolTempFileTracker get instance => _instance ??= ToolTempFileTracker._();
4232

43-
void _error(String message) {
44-
if (_errorCallback != null) {
45-
_errorCallback(message);
46-
}
47-
}
48-
49-
Future<File> _createTemporaryFile() async {
33+
int _temporaryFileCount = 0;
34+
Future<File> createTemporaryFile() async {
5035
_temporaryFileCount++;
5136
File tempFile = new File(pathLib.join(
52-
(await temporaryDirectory).absolute.path,
37+
temporaryDirectory.absolute.path,
5338
'input_$_temporaryFileCount'));
5439
await tempFile.create(recursive: true);
5540
return tempFile;
5641
}
5742

58-
/// Must be called when the ToolRunner is no longer needed. Ideally, this is
59-
/// called in the finally section of a try/finally.
60-
///
61-
/// This will remove any temporary files created by the tool runner.
62-
void dispose() {
63-
if (_temporaryDirectory != null) disposeAsync(_temporaryDirectory);
64-
}
65-
66-
/// Avoid blocking on I/O for cleanups.
67-
static Future<void> disposeAsync(Future<Directory> temporaryDirectory) async {
68-
Directory tempDir = await temporaryDirectory;
69-
if (await tempDir.exists()) {
70-
return tempDir.delete(recursive: true);
43+
/// Call once no more files are to be created.
44+
Future<void> dispose() async {
45+
if (temporaryDirectory.existsSync()) {
46+
return temporaryDirectory.delete(recursive: true);
7147
}
7248
}
49+
}
50+
51+
/// A helper class for running external tools.
52+
class ToolRunner {
53+
/// Creates a new ToolRunner.
54+
///
55+
/// Takes a [toolConfiguration] that describes all of the available tools.
56+
/// An optional `errorCallback` will be called for each error message
57+
/// generated by the tool.
58+
ToolRunner(this.toolConfiguration);
59+
60+
final ToolConfiguration toolConfiguration;
7361

7462
void _runSetup(
75-
String name, ToolDefinition tool, Map<String, String> environment) async {
63+
String name, ToolDefinition tool, Map<String, String> environment,
64+
ToolErrorCallback toolErrorCallback) async {
7665
bool isDartSetup = ToolDefinition.isDartExecutable(tool.setupCommand[0]);
7766
var args = tool.setupCommand.toList();
7867
String commandPath;
@@ -82,18 +71,19 @@ class ToolRunner {
8271
} else {
8372
commandPath = args.removeAt(0);
8473
}
85-
await _runProcess(name, '', commandPath, args, environment);
74+
await _runProcess(name, '', commandPath, args, environment, toolErrorCallback);
8675
tool.setupComplete = true;
8776
}
8877

8978
Future<String> _runProcess(String name, String content, String commandPath,
90-
List<String> args, Map<String, String> environment) async {
79+
List<String> args, Map<String, String> environment,
80+
ToolErrorCallback toolErrorCallback) async {
9181
String commandString() => ([commandPath] + args).join(' ');
9282
try {
9383
ProcessResult result =
9484
await Process.run(commandPath, args, environment: environment);
9585
if (result.exitCode != 0) {
96-
_error('Tool "$name" returned non-zero exit code '
86+
toolErrorCallback('Tool "$name" returned non-zero exit code '
9787
'(${result.exitCode}) when run as '
9888
'"${commandString()}" from ${Directory.current}\n'
9989
'Input to $name was:\n'
@@ -104,7 +94,7 @@ class ToolRunner {
10494
return result.stdout;
10595
}
10696
} on ProcessException catch (exception) {
107-
_error('Failed to run tool "$name" as '
97+
toolErrorCallback('Failed to run tool "$name" as '
10898
'"${commandString()}": $exception\n'
10999
'Input to $name was:\n'
110100
'$content');
@@ -118,26 +108,26 @@ class ToolRunner {
118108
///
119109
/// The [args] must not be null, and it must have at least one member (the name
120110
/// of the tool).
121-
Future<String> run(List<String> args,
111+
Future<String> run(List<String> args, ToolErrorCallback toolErrorCallback,
122112
{String content, Map<String, String> environment}) async {
123113
Future runner;
124114
// Prevent too many tools from running simultaneously.
125115
await _toolTracker.addFutureFromClosure(() {
126-
runner = _run(args, content: content, environment: environment);
116+
runner = _run(args, toolErrorCallback, content: content, environment: environment);
127117
return runner;
128118
});
129119
return runner;
130120
}
131121

132-
Future<String> _run(List<String> args,
122+
Future<String> _run(List<String> args, ToolErrorCallback toolErrorCallback,
133123
{String content, Map<String, String> environment}) async {
134124
assert(args != null);
135125
assert(args.isNotEmpty);
136126
content ??= '';
137127
environment ??= <String, String>{};
138128
var tool = args.removeAt(0);
139129
if (!toolConfiguration.tools.containsKey(tool)) {
140-
_error('Unable to find definition for tool "$tool" in tool map. '
130+
toolErrorCallback('Unable to find definition for tool "$tool" in tool map. '
141131
'Did you add it to dartdoc_options.yaml?');
142132
return '';
143133
}
@@ -152,7 +142,7 @@ class ToolRunner {
152142
// file before running the tool synchronously.
153143

154144
// Write the content to a temp file.
155-
var tmpFile = await _createTemporaryFile();
145+
var tmpFile = await ToolTempFileTracker.instance.createTemporaryFile();
156146
await tmpFile.writeAsString(content);
157147

158148
// Substitute the temp filename for the "$INPUT" token, and all of the other
@@ -188,7 +178,7 @@ class ToolRunner {
188178
}
189179

190180
if (toolDefinition.setupCommand != null && !toolDefinition.setupComplete)
191-
await _runSetup(tool, toolDefinition, envWithInput);
181+
await _runSetup(tool, toolDefinition, envWithInput, toolErrorCallback);
192182

193183
argsWithInput = toolArgs + argsWithInput;
194184
var commandPath;
@@ -201,13 +191,14 @@ class ToolRunner {
201191
} else {
202192
commandPath = argsWithInput.removeAt(0);
203193
}
194+
204195
if (callCompleter != null) {
205196
return _runProcess(
206-
tool, content, commandPath, argsWithInput, envWithInput)
197+
tool, content, commandPath, argsWithInput, envWithInput, toolErrorCallback)
207198
.whenComplete(callCompleter);
208199
} else {
209200
return _runProcess(
210-
tool, content, commandPath, argsWithInput, envWithInput);
201+
tool, content, commandPath, argsWithInput, envWithInput, toolErrorCallback);
211202
}
212203
}
213204
}

test/tool_runner_test.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ void main() {
2020
File setupFile;
2121

2222
ToolRunner runner;
23+
ToolTempFileTracker tracker;
24+
ToolErrorCallback errorCallback;
2325
final errors = <String>[];
2426

2527
setUpAll(() async {
@@ -81,11 +83,12 @@ echo:
8183
'missing': new ToolDefinition(['/a/missing/executable'], null, "missing"),
8284
});
8385

84-
runner = new ToolRunner(toolMap, (String message) => errors.add(message));
86+
runner = new ToolRunner(toolMap);
87+
errorCallback = (String message) => errors.add(message);
8588
});
8689
tearDownAll(() {
8790
tempDir?.deleteSync(recursive: true);
88-
runner?.dispose();
91+
tracker?.dispose();
8992
SnapshotCache.instance.dispose();
9093
setupFile = null;
9194
tempDir = null;
@@ -100,6 +103,7 @@ echo:
100103
test('can invoke a Dart tool, and second run is a snapshot.', () async {
101104
var result = await runner.run(
102105
['drill', r'--file=$INPUT'],
106+
errorCallback,
103107
content: 'TEST INPUT',
104108
);
105109
expect(errors, isEmpty);
@@ -109,6 +113,7 @@ echo:
109113
expect(setupFile.existsSync(), isFalse);
110114
result = await runner.run(
111115
['drill', r'--file=$INPUT'],
116+
errorCallback,
112117
content: 'TEST INPUT 2',
113118
);
114119
expect(errors, isEmpty);
@@ -120,6 +125,7 @@ echo:
120125
test('can invoke a Dart tool', () async {
121126
var result = await runner.run(
122127
['drill', r'--file=$INPUT'],
128+
errorCallback,
123129
content: 'TEST INPUT',
124130
);
125131
expect(errors, isEmpty);
@@ -131,6 +137,7 @@ echo:
131137
test('can invoke a non-Dart tool', () async {
132138
String result = await runner.run(
133139
['non_dart', '--version'],
140+
errorCallback,
134141
content: 'TEST INPUT',
135142
);
136143
expect(errors, isEmpty);
@@ -139,6 +146,7 @@ echo:
139146
test('can invoke a pre-snapshotted tool', () async {
140147
var result = await runner.run(
141148
['snapshot_drill', r'--file=$INPUT'],
149+
errorCallback,
142150
content: 'TEST INPUT',
143151
);
144152
expect(errors, isEmpty);
@@ -148,6 +156,7 @@ echo:
148156
test('can invoke a tool with a setup action', () async {
149157
var result = await runner.run(
150158
['setup_drill', r'--file=$INPUT'],
159+
errorCallback,
151160
content: 'TEST INPUT',
152161
);
153162
expect(errors, isEmpty);
@@ -158,6 +167,7 @@ echo:
158167
test('fails if tool not in tool map', () async {
159168
String result = await runner.run(
160169
['hammer', r'--file=$INPUT'],
170+
errorCallback,
161171
content: 'TEST INPUT',
162172
);
163173
expect(errors, isNotEmpty);
@@ -168,6 +178,7 @@ echo:
168178
test('fails if tool returns non-zero status', () async {
169179
String result = await runner.run(
170180
['drill', r'--file=/a/missing/file'],
181+
errorCallback,
171182
content: 'TEST INPUT',
172183
);
173184
expect(errors, isNotEmpty);
@@ -177,6 +188,7 @@ echo:
177188
test("fails if tool in tool map doesn't exist", () async {
178189
String result = await runner.run(
179190
['missing'],
191+
errorCallback,
180192
content: 'TEST INPUT',
181193
);
182194
expect(errors, isNotEmpty);

0 commit comments

Comments
 (0)