Chromium Code Reviews| Index: pkg/analyzer/lib/src/task/driver.dart |
| diff --git a/pkg/analyzer/lib/src/task/driver.dart b/pkg/analyzer/lib/src/task/driver.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..bd20615ad2d852ca607dee420663f96ed9b7ad7e |
| --- /dev/null |
| +++ b/pkg/analyzer/lib/src/task/driver.dart |
| @@ -0,0 +1,411 @@ |
| +// Copyright (c) 2015, 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. |
| + |
| +library analyzer.src.task.driver; |
| + |
| +import 'dart:async'; |
| +import 'dart:collection'; |
| + |
| +import 'package:analyzer/src/context/cache.dart'; |
| +import 'package:analyzer/src/generated/engine.dart' hide AnalysisTask; |
| +import 'package:analyzer/src/generated/java_engine.dart'; |
| +import 'package:analyzer/src/task/inputs.dart'; |
| +import 'package:analyzer/src/task/manager.dart'; |
| +import 'package:analyzer/task/model.dart'; |
| + |
| +/** |
| + * An object that is used to cause analysis to be performed until all of the |
| + * required analysis information has been computed. |
| + */ |
| +class AnalysisDriver { |
| + /** |
| + * The task manager used to figure out how to compute analysis results. |
| + */ |
| + final TaskManager taskManager; |
| + |
| + /** |
| + * The context in which analysis is to be performed. |
| + */ |
| + final ExtendedAnalysisContext context; |
| + |
| + /** |
| + * The work order that was previously computed but that has not yet been |
| + * completed. |
| + */ |
| + WorkOrder currentWorkOrder; |
| + |
| + /** |
| + * The controller that is notified when a task is started. |
| + */ |
| + StreamController<AnalysisTask> _onTaskStartedController; |
| + |
| + /** |
| + * The controller that is notified when a task is complete. |
| + */ |
| + StreamController<AnalysisTask> _onTaskCompletedController; |
| + |
| + /** |
| + * Initialize a newly created driver to use the tasks know to the given |
| + * [taskManager] to perform analysis in the given [context]. |
| + */ |
| + AnalysisDriver(this.taskManager, this.context) { |
| + _onTaskStartedController = new StreamController.broadcast(); |
| + _onTaskCompletedController = new StreamController.broadcast(); |
| + } |
| + |
| + /** |
| + * The stream that is notified when a task is complete. |
| + */ |
| + Stream<AnalysisTask> get onTaskCompleted => _onTaskCompletedController.stream; |
| + |
| + /** |
| + * The stream that is notified when a task is started. |
| + */ |
| + Stream<AnalysisTask> get onTaskStarted => _onTaskStartedController.stream; |
| + |
| + /** |
| + * Perform work until the given [result] has been computed for the given |
| + * [target]. |
| + */ |
| + void computeResult(AnalysisTarget target, ResultDescriptor result) { |
| + WorkOrder workOrder = createWorkOrderForResult(target, result); |
| + if (workOrder != null) { |
| + while (workOrder.moveNext()) { |
| + performWorkItem(workOrder.current); |
| + } |
| + } |
| + } |
| + |
| + /** |
| + * Return the work order describing the work that should be getting worked on, |
| + * or `null` if there is currently no work to be done. |
| + */ |
| + WorkOrder createNextWorkOrder() { |
| + // |
| + // TODO(brianwilkerson) This is an inefficient implementation. We need to |
| + // port over the concept of the WorkManager to manage the list of sources |
| + // for which some work needs to be performed so that we do not waste time |
| + // repeatedly looking at the same completed sources to see whether there is |
| + // work that needs to be done. |
|
Paul Berry
2015/03/03 17:21:32
An easy (but slightly hacky) way to achieve most o
Brian Wilkerson
2015/03/03 21:21:17
True. I think WorkManager is less "hacky" and shou
|
| + // |
| + for (AnalysisTarget target in context.priorityTargets) { |
|
Paul Berry
2015/03/03 17:21:32
I'm surprised to see only two levels of priority h
Brian Wilkerson
2015/03/03 21:21:17
That was probably done after I copied this code. I
|
| + WorkOrder workOrder = createWorkOrderForTarget(target, true); |
| + if (workOrder != null) { |
| + return workOrder; |
| + } |
| + } |
| + for (AnalysisTarget target in context.explicitTargets) { |
| + WorkOrder workOrder = createWorkOrderForTarget(target, false); |
| + if (workOrder != null) { |
| + return workOrder; |
| + } |
| + } |
| + return null; |
| + } |
| + |
| + /** |
| + * Create a work order that will produce the given [result] for the given |
| + * [target]. Return the work order that was created, or `null` if the result |
| + * has already been computed. |
| + */ |
| + WorkOrder createWorkOrderForResult(AnalysisTarget target, |
| + ResultDescriptor result) { |
| + CacheEntry entry = context.getCacheEntry(target); |
| + CacheState state = entry.getState(result); |
| + if (state == CacheState.VALID || |
| + state == CacheState.ERROR || |
| + state == CacheState.IN_PROCESS) { |
| + return null; |
| + } |
| + return new WorkOrder( |
| + taskManager, |
| + new WorkItem(context, target, taskManager.findTask(target, result))); |
| + } |
| + |
| + /** |
| + * Create a work order that will produce the required analysis results for |
| + * the given [target]. If [isPriority] is true, then the target is a priority |
| + * target. Return the work order that was created, or `null` if there is no |
| + * further work that needs to be done for the given target. |
| + */ |
| + WorkOrder createWorkOrderForTarget(AnalysisTarget target, bool isPriority) { |
| + for (ResultDescriptor result in taskManager.generalResults) { |
| + WorkOrder workOrder = createWorkOrderForResult(target, result); |
| + if (workOrder != null) { |
| + return workOrder; |
| + } |
| + } |
| + if (isPriority) { |
| + for (ResultDescriptor result in taskManager.priorityResults) { |
| + WorkOrder workOrder = createWorkOrderForResult(target, result); |
| + if (workOrder != null) { |
| + return workOrder; |
| + } |
| + } |
| + } |
| + return null; |
| + } |
| + |
| + /** |
| + * Perform the next analysis task, and return `true` if there is more work to |
| + * be done in order to compute all of the required analysis information. |
| + */ |
| + bool performAnalysisTask() { |
| + // |
| + // TODO(brianwilkerson) This implementaiton does not allow us to prioritize |
| + // work across contexts. What we need is a way for an external client to ask |
| + // to have all priority files analyzed for each context, then ask for normal |
| + // files to be analyzed. There are a couple of ways to do this. |
| + // |
| + // First, we could add a "bool priorityOnly" parameter to this method and |
| + // return null here when it is true. |
| + // |
| + // Second, we could add a concept of a priority order and (externally) run |
| + // through the priorities from highest to lowest. That would be a nice |
| + // generalization of the previous idea, but it isn't clear that we need the |
| + // generality. |
| + // |
|
Paul Berry
2015/03/03 17:21:32
A third approach, which I personally favor, would
Brian Wilkerson
2015/03/03 21:21:18
Added to the TODO.
|
| + if (currentWorkOrder == null) { |
| + currentWorkOrder = createNextWorkOrder(); |
| + } else if (currentWorkOrder.moveNext()) { |
| + performWorkItem(currentWorkOrder.current); |
| + } else { |
| + currentWorkOrder = createNextWorkOrder(); |
| + } |
| + return currentWorkOrder != null; |
| + } |
| + |
| + /** |
| + * Perform the given work item. |
| + */ |
| + void performWorkItem(WorkItem item) { |
| + if (item.exception != null) { |
| + // Mark all of the results that the task would have computed as being in |
| + // ERROR with the exception recorded on the work item. |
| + CacheEntry targetEntry = context.getCacheEntry(item.target); |
| + targetEntry.exception = item.exception; |
| + for (ResultDescriptor result in item.descriptor.results) { |
| + targetEntry.setState(result, CacheState.ERROR); |
| + } |
| + return; |
| + } |
| + // Otherwise, perform the task. |
| + AnalysisTask task = item.buildTask(); |
| + _onTaskStartedController.add(task); |
| + task.perform(); |
| + CacheEntry entry = context.getCacheEntry(task.target); |
| + if (task.caughtException == null) { |
| + Map<ResultDescriptor, dynamic> outputs = task.outputs; |
| + for (ResultDescriptor result in task.descriptor.results) { |
| + // TODO(brianwilkerson) We could check here that a value was produced |
| + // and throw an exception if not (unless we want to allow null values). |
| + entry.setValue(result, outputs[result]); |
| + } |
| + } else { |
| + entry.exception = task.caughtException; |
| + for (ResultDescriptor result in task.descriptor.results) { |
| + entry.setState(result, CacheState.ERROR); |
| + } |
| + } |
| + _onTaskCompletedController.add(task); |
| + } |
| + |
| + /** |
| + * Reset the state of the driver in response to a change in the state of one |
| + * or more analysis targets. This will cause any analysis that was currently |
| + * in process to be stopped and for analysis to resume based on the new state. |
| + */ |
| + void reset() { |
| + currentWorkOrder = null; |
| + } |
| +} |
| + |
| +/** |
| + * A place to define the behaviors that need to be added to |
| + * [InternalAnalysisContext]. |
| + */ |
| +abstract class ExtendedAnalysisContext implements InternalAnalysisContext { |
| + List<AnalysisTarget> get explicitTargets; |
| + List<AnalysisTarget> get priorityTargets; |
| + CacheEntry getCacheEntry(AnalysisTarget target); |
| +} |
| + |
| +/** |
| + * A description of a single anaysis task that can be performed to advance |
| + * analysis. |
| + */ |
| +class WorkItem { |
| + /** |
| + * The context in which the task will be performed. |
| + */ |
| + final ExtendedAnalysisContext context; |
| + |
| + /** |
| + * The target for which a task is to be performed. |
| + */ |
| + final AnalysisTarget target; |
| + |
| + /** |
| + * A description of the task to be performed. |
| + */ |
| + final TaskDescriptor descriptor; |
| + |
| + /** |
| + * An iterator used to iterate over the descriptors of the inputs to the task, |
| + * or `null` if all of the inputs have been collected and the task can be |
| + * created. |
| + */ |
| + TaskInputBuilder builder; |
| + |
| + /** |
| + * The inputs to the task that have been computed. |
| + */ |
| + Map<String, dynamic> inputs; |
| + |
| + /** |
| + * The exception that was found while trying to populate the inputs. If this |
| + * field is non-`null`, then the task cannot be performed and all of the |
| + * results that this task would have computed need to be marked as being in |
| + * ERROR with this exception. |
| + */ |
| + CaughtException exception = null; |
| + |
| + /** |
| + * Initialize a newly created work item to compute the inputs for the task |
| + * described by the given descriptor. |
| + */ |
| + WorkItem(this.context, this.target, this.descriptor) { |
| + Map<String, TaskInput> inputDescriptors = |
| + descriptor.createTaskInputs(target); |
| + builder = new TopLevelTaskInputBuilder(inputDescriptors); |
| + if (!builder.moveNext()) { |
| + builder = null; |
| + } |
| + inputs = new HashMap<String, dynamic>(); |
| + } |
| + |
| + /** |
| + * Build the task represented by this work item. |
| + */ |
| + AnalysisTask buildTask() { |
| + if (builder != null) { |
| + throw new StateError("some inputs have not been computed"); |
| + } |
| + return descriptor.createTask(context, target, inputs); |
| + } |
| + |
| + /** |
| + * Gather all of the inputs needed to perform the task. |
| + * |
| + * If at least one of the inputs have not yet been computed, return a work |
| + * item that can be used to generate that input to indicate that the caller |
| + * should perform the returned item's task before returning to gathering |
| + * inputs for this item's task. |
| + * |
| + * If all of the inputs have been gathered, return `null` to indicate that the |
| + * client should build and perform the task. A value of `null` will also be |
| + * returned if some of the inputs cannot be computed and the task cannot be |
| + * performed. Callers can differentiate between these cases by checking the |
| + * [exception] field. If the field is `null`, then the task can be performed; |
| + * if the field is non-`null` then the task cannot be performed and all of the |
| + * tasks' results should be marked as being in ERROR. |
| + */ |
| + WorkItem gatherInputs(TaskManager taskManager) { |
| + while (builder != null) { |
| + // |
| + // TODO(brianwilkerson) Capture information about which inputs were used |
| + // to compute the results. This information can later be used to compute |
| + // which results depend on a given result, and hence which results need to |
| + // be invalidated when one result is invalidated. |
| + // |
| + AnalysisTarget inputTarget = builder.currentTarget; |
| + ResultDescriptor inputResult = builder.currentResult; |
| + CacheEntry inputEntry = context.getCacheEntry(inputTarget); |
| + CacheState inputState = inputEntry.getState(inputResult); |
| + if (inputState == CacheState.ERROR) { |
| + exception = inputEntry.exception; |
| + return null; |
| + } else if (inputState == CacheState.IN_PROCESS) { |
| + // |
| + // TODO(brianwilkerson) Implement this case. |
| + // |
| + // One possibility would be to return a WorkItem that would perform a |
| + // no-op task in order to cause us to come back to this work item on the |
| + // next iteration. It would be more efficient, in general, to push this |
| + // input onto a waiting list and proceed to the next input so that work |
| + // could proceed, but given that the only result that can currently be |
| + // IN_PROCESS is CONTENT, I don't know that it's worth the extra effort |
| + // to implement the general solution at this point. |
| + // |
| + } else if (inputState != CacheState.VALID) { |
| + try { |
| + TaskDescriptor descriptor = |
| + taskManager.findTask(inputTarget, inputResult); |
| + return new WorkItem(context, inputTarget, descriptor); |
| + } on AnalysisException catch (exception, stackTrace) { |
| + this.exception = new CaughtException(exception, stackTrace); |
| + return null; |
| + } |
| + } |
| + builder.currentValue = inputEntry.getValue(inputResult); |
| + if (!builder.moveNext()) { |
| + inputs = builder.inputValue; |
| + builder = null; |
| + } |
| + } |
| + return null; |
| + } |
| +} |
| + |
| +/** |
| + * A description of the work to be done to compute a desired analysis result. |
|
Paul Berry
2015/03/03 17:21:32
It would me nice to have a comment explaining that
Brian Wilkerson
2015/03/03 21:21:18
Done
|
| + */ |
| +class WorkOrder implements Iterator<WorkItem> { |
| + /** |
| + * The task manager used to build work items. |
| + */ |
| + final TaskManager taskManager; |
| + |
| + /** |
| + * A list containing the work items that are being prepared for being worked. |
| + */ |
| + final List<WorkItem> pendingItems = <WorkItem>[]; |
| + |
| + /** |
| + * The current work item. |
| + */ |
| + WorkItem currentItem; |
| + |
| + /** |
| + * Initialize a newly created work order to compute the result described by |
| + * the given work item. |
| + */ |
| + WorkOrder(this.taskManager, WorkItem item) { |
| + pendingItems.add(item); |
| + } |
| + |
| + @override |
| + WorkItem get current { |
| + WorkItem item = currentItem; |
| + currentItem = null; |
|
Paul Berry
2015/03/03 17:21:32
This seems dangerous because it violates the usual
Brian Wilkerson
2015/03/03 21:21:18
Done
|
| + return item; |
| + } |
| + |
| + @override |
| + bool moveNext() { |
| + if (currentItem == null) { |
| + if (pendingItems.isEmpty) { |
| + return false; |
| + } |
| + currentItem = pendingItems.removeLast(); |
| + } |
| + WorkItem childItem = currentItem.gatherInputs(taskManager); |
| + while (childItem != null) { |
| + pendingItems.add(currentItem); |
| + currentItem = childItem; |
| + childItem = currentItem.gatherInputs(taskManager); |
| + } |
| + return true; |
| + } |
| +} |