2010-06-15 12:43:46 +08:00
|
|
|
package org.broadinstitute.sting.queue.engine
|
|
|
|
|
|
2010-07-16 06:32:48 +08:00
|
|
|
import org.jgrapht.traverse.TopologicalOrderIterator
|
2010-06-15 12:43:46 +08:00
|
|
|
import org.jgrapht.graph.SimpleDirectedGraph
|
|
|
|
|
import scala.collection.JavaConversions
|
2010-06-23 02:39:20 +08:00
|
|
|
import scala.collection.JavaConversions._
|
|
|
|
|
import org.broadinstitute.sting.queue.function.scattergather.ScatterGatherableFunction
|
2010-08-10 00:42:48 +08:00
|
|
|
import org.broadinstitute.sting.queue.util.Logging
|
2010-06-23 02:39:20 +08:00
|
|
|
import org.jgrapht.alg.CycleDetector
|
|
|
|
|
import org.jgrapht.EdgeFactory
|
2010-07-16 06:32:48 +08:00
|
|
|
import org.jgrapht.ext.DOTExporter
|
2010-08-10 00:42:48 +08:00
|
|
|
import java.io.File
|
2010-08-12 05:58:26 +08:00
|
|
|
import org.jgrapht.event.{TraversalListenerAdapter, EdgeTraversalEvent}
|
|
|
|
|
import org.broadinstitute.sting.queue.{QSettings, QException}
|
|
|
|
|
import org.broadinstitute.sting.queue.function.{DispatchWaitFunction, MappingFunction, CommandLineFunction, QFunction}
|
2010-06-15 12:43:46 +08:00
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* The internal dependency tracker between sets of function input and output files.
|
|
|
|
|
*/
|
2010-06-15 12:43:46 +08:00
|
|
|
class QGraph extends Logging {
|
|
|
|
|
var dryRun = true
|
|
|
|
|
var bsubAllJobs = false
|
2010-06-29 03:52:17 +08:00
|
|
|
var bsubWaitJobs = false
|
2010-08-12 05:58:26 +08:00
|
|
|
var skipUpToDateJobs = false
|
|
|
|
|
var dotFile: File = _
|
|
|
|
|
var expandedDotFile: File = _
|
|
|
|
|
var qSettings: QSettings = _
|
2010-08-13 23:54:08 +08:00
|
|
|
var debugMode = false
|
2010-08-12 05:58:26 +08:00
|
|
|
private val jobGraph = newGraph
|
2010-06-15 12:43:46 +08:00
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Adds a QScript created CommandLineFunction to the graph.
|
|
|
|
|
* @param command Function to add to the graph.
|
|
|
|
|
*/
|
2010-06-15 12:43:46 +08:00
|
|
|
def add(command: CommandLineFunction) {
|
2010-06-26 04:51:13 +08:00
|
|
|
addFunction(command)
|
2010-06-15 12:43:46 +08:00
|
|
|
}
|
|
|
|
|
|
2010-08-12 05:58:26 +08:00
|
|
|
/**
|
|
|
|
|
* Checks the functions for missing values and the graph for cyclic dependencies and then runs the functions in the graph.
|
|
|
|
|
*/
|
|
|
|
|
def run = {
|
|
|
|
|
fill
|
|
|
|
|
if (dotFile != null)
|
|
|
|
|
renderToDot(dotFile)
|
|
|
|
|
var numMissingValues = validate
|
|
|
|
|
|
|
|
|
|
if (numMissingValues == 0 && bsubAllJobs) {
|
|
|
|
|
logger.debug("Scatter gathering jobs.")
|
|
|
|
|
var scatterGathers = List.empty[ScatterGatherableFunction]
|
|
|
|
|
loop({
|
|
|
|
|
case scatterGather: ScatterGatherableFunction if (scatterGather.scatterGatherable) =>
|
|
|
|
|
scatterGathers :+= scatterGather
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
var addedFunctions = List.empty[CommandLineFunction]
|
|
|
|
|
for (scatterGather <- scatterGathers) {
|
|
|
|
|
val functions = scatterGather.generateFunctions()
|
2010-08-13 23:54:08 +08:00
|
|
|
if (this.debugMode)
|
|
|
|
|
logger.debug("Scattered into %d parts: %n%s".format(functions.size, functions.mkString("%n".format())))
|
2010-08-12 05:58:26 +08:00
|
|
|
addedFunctions ++= functions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.jobGraph.removeAllEdges(scatterGathers)
|
|
|
|
|
prune
|
|
|
|
|
addedFunctions.foreach(this.addFunction(_))
|
|
|
|
|
|
|
|
|
|
fill
|
|
|
|
|
val scatterGatherDotFile = if (expandedDotFile != null) expandedDotFile else dotFile
|
|
|
|
|
if (scatterGatherDotFile != null)
|
|
|
|
|
renderToDot(scatterGatherDotFile)
|
|
|
|
|
numMissingValues = validate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val isReady = numMissingValues == 0
|
|
|
|
|
|
|
|
|
|
if (isReady || this.dryRun)
|
|
|
|
|
runJobs
|
|
|
|
|
|
|
|
|
|
if (numMissingValues > 0) {
|
|
|
|
|
logger.error("Total missing values: " + numMissingValues)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isReady && this.dryRun) {
|
|
|
|
|
logger.info("Dry run completed successfully!")
|
|
|
|
|
logger.info("Re-run with \"-run\" to execute the functions.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Walks up the graph looking for the previous LsfJobs.
|
|
|
|
|
* @param function Function to examine for a previous command line job.
|
|
|
|
|
* @param qGraph The graph that contains the jobs.
|
|
|
|
|
* @return A list of prior jobs.
|
|
|
|
|
*/
|
|
|
|
|
def previousJobs(function: QFunction) : List[CommandLineFunction] = {
|
|
|
|
|
var previous = List.empty[CommandLineFunction]
|
|
|
|
|
|
|
|
|
|
val source = this.jobGraph.getEdgeSource(function)
|
|
|
|
|
for (incomingEdge <- this.jobGraph.incomingEdgesOf(source)) {
|
|
|
|
|
incomingEdge match {
|
|
|
|
|
|
|
|
|
|
// Stop recursing when we find a job along the edge and return its job id
|
|
|
|
|
case commandLineFunction: CommandLineFunction => previous :+= commandLineFunction
|
|
|
|
|
|
|
|
|
|
// For any other type of edge find the LSF jobs preceding the edge
|
|
|
|
|
case qFunction: QFunction => previous ++= previousJobs(qFunction)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
previous
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fills in the graph using mapping functions, then removes out of date
|
|
|
|
|
* jobs, then cleans up mapping functions and nodes that aren't need.
|
|
|
|
|
*/
|
|
|
|
|
private def fill = {
|
|
|
|
|
fillIn
|
|
|
|
|
if (skipUpToDateJobs)
|
|
|
|
|
removeUpToDate
|
|
|
|
|
prune
|
|
|
|
|
}
|
|
|
|
|
|
2010-06-15 12:43:46 +08:00
|
|
|
/**
|
|
|
|
|
* Looks through functions with multiple inputs and outputs and adds mapping functions for single inputs and outputs.
|
|
|
|
|
*/
|
2010-08-12 05:58:26 +08:00
|
|
|
private def fillIn = {
|
2010-06-15 12:43:46 +08:00
|
|
|
// clone since edgeSet is backed by the graph
|
2010-08-12 05:58:26 +08:00
|
|
|
JavaConversions.asSet(jobGraph.edgeSet).clone.foreach {
|
|
|
|
|
case cmd: CommandLineFunction => {
|
|
|
|
|
addCollectionOutputs(cmd.outputs)
|
|
|
|
|
addCollectionInputs(cmd.inputs)
|
|
|
|
|
}
|
|
|
|
|
case map: MappingFunction => /* do nothing for mapping functions */
|
2010-06-23 02:39:20 +08:00
|
|
|
}
|
2010-08-12 05:58:26 +08:00
|
|
|
}
|
2010-06-15 12:43:46 +08:00
|
|
|
|
2010-08-12 05:58:26 +08:00
|
|
|
/**
|
|
|
|
|
* Removes functions that are up to date.
|
|
|
|
|
*/
|
|
|
|
|
private def removeUpToDate = {
|
|
|
|
|
var upToDateJobs = Set.empty[CommandLineFunction]
|
|
|
|
|
loop({
|
|
|
|
|
case f if (upToDate(f, upToDateJobs)) => {
|
|
|
|
|
logger.info("Skipping command because it is up to date: %n%s".format(f.commandLine))
|
|
|
|
|
upToDateJobs += f
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
for (upToDateJob <- upToDateJobs)
|
|
|
|
|
jobGraph.removeEdge(upToDateJob)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns true if the all previous functions in the graph are up to date, and the function is up to date.
|
|
|
|
|
*/
|
|
|
|
|
private def upToDate(commandLineFunction: CommandLineFunction, upToDateJobs: Set[CommandLineFunction]) = {
|
|
|
|
|
this.previousJobs(commandLineFunction).forall(upToDateJobs.contains(_)) && commandLineFunction.upToDate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2010-08-13 23:54:08 +08:00
|
|
|
* Removes mapping edges that aren't being used, and nodes that don't belong to anything.
|
2010-08-12 05:58:26 +08:00
|
|
|
*/
|
|
|
|
|
private def prune = {
|
2010-06-23 02:39:20 +08:00
|
|
|
var pruning = true
|
|
|
|
|
while (pruning) {
|
|
|
|
|
pruning = false
|
|
|
|
|
val filler = jobGraph.edgeSet.filter(isFiller(_))
|
|
|
|
|
if (filler.size > 0) {
|
|
|
|
|
jobGraph.removeAllEdges(filler)
|
|
|
|
|
pruning = true
|
|
|
|
|
}
|
2010-06-15 12:43:46 +08:00
|
|
|
}
|
2010-06-23 02:39:20 +08:00
|
|
|
|
|
|
|
|
jobGraph.removeAllVertices(jobGraph.vertexSet.filter(isOrphan(_)))
|
2010-06-15 12:43:46 +08:00
|
|
|
}
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
2010-08-12 05:58:26 +08:00
|
|
|
* Validates that the functions in the graph have no missing values and that there are no cycles.
|
|
|
|
|
* @return Number of missing values.
|
2010-08-10 00:42:48 +08:00
|
|
|
*/
|
2010-08-12 05:58:26 +08:00
|
|
|
private def validate = {
|
|
|
|
|
var numMissingValues = 0
|
|
|
|
|
JavaConversions.asSet(jobGraph.edgeSet).foreach {
|
|
|
|
|
case cmd: CommandLineFunction =>
|
|
|
|
|
val missingFieldValues = cmd.missingFields
|
|
|
|
|
if (missingFieldValues.size > 0) {
|
|
|
|
|
numMissingValues += missingFieldValues.size
|
|
|
|
|
logger.error("Missing %s values for function: %s".format(missingFieldValues.size, cmd.commandLine))
|
|
|
|
|
for (missing <- missingFieldValues)
|
|
|
|
|
logger.error(" " + missing)
|
|
|
|
|
}
|
|
|
|
|
case map: MappingFunction => /* do nothing for mapping functions */
|
2010-08-10 00:42:48 +08:00
|
|
|
}
|
|
|
|
|
|
2010-06-23 02:39:20 +08:00
|
|
|
val detector = new CycleDetector(jobGraph)
|
|
|
|
|
if (detector.detectCycles) {
|
|
|
|
|
logger.error("Cycles were detected in the graph:")
|
|
|
|
|
for (cycle <- detector.findCycles)
|
|
|
|
|
logger.error(" " + cycle)
|
2010-08-12 05:58:26 +08:00
|
|
|
throw new QException("Cycles were detected in the graph.")
|
2010-06-23 02:39:20 +08:00
|
|
|
}
|
|
|
|
|
|
2010-08-12 05:58:26 +08:00
|
|
|
numMissingValues
|
|
|
|
|
}
|
2010-08-10 00:42:48 +08:00
|
|
|
|
2010-08-12 05:58:26 +08:00
|
|
|
/**
|
|
|
|
|
* Runs the jobs by traversing the graph.
|
|
|
|
|
*/
|
|
|
|
|
private def runJobs = {
|
|
|
|
|
val runner = if (bsubAllJobs) new LsfJobRunner else new ShellJobRunner
|
|
|
|
|
|
|
|
|
|
val numJobs = JavaConversions.asSet(jobGraph.edgeSet).filter(_.isInstanceOf[CommandLineFunction]).size
|
|
|
|
|
|
|
|
|
|
logger.info("Number of jobs: %s".format(numJobs))
|
2010-08-13 23:54:08 +08:00
|
|
|
if (this.debugMode) {
|
2010-08-12 05:58:26 +08:00
|
|
|
val numNodes = jobGraph.vertexSet.size
|
2010-08-13 23:54:08 +08:00
|
|
|
logger.debug("Number of nodes: %s".format(numNodes))
|
2010-08-10 00:42:48 +08:00
|
|
|
}
|
2010-08-12 05:58:26 +08:00
|
|
|
var numNodes = 0
|
2010-08-10 00:42:48 +08:00
|
|
|
|
2010-08-12 05:58:26 +08:00
|
|
|
loop(
|
|
|
|
|
edgeFunction = { case f => runner.run(f, this) },
|
|
|
|
|
nodeFunction = {
|
|
|
|
|
case node => {
|
2010-08-13 23:54:08 +08:00
|
|
|
if (this.debugMode)
|
|
|
|
|
logger.debug("Visiting: " + node)
|
2010-08-12 05:58:26 +08:00
|
|
|
numNodes += 1
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2010-08-13 23:54:08 +08:00
|
|
|
if (this.debugMode)
|
|
|
|
|
logger.debug("Done walking %s nodes.".format(numNodes))
|
2010-08-12 05:58:26 +08:00
|
|
|
|
|
|
|
|
if (bsubAllJobs && bsubWaitJobs) {
|
|
|
|
|
logger.info("Waiting for jobs to complete.")
|
|
|
|
|
val wait = new DispatchWaitFunction
|
|
|
|
|
wait.qSettings = this.qSettings
|
|
|
|
|
wait.freeze
|
|
|
|
|
runner.run(wait, this)
|
2010-08-10 00:42:48 +08:00
|
|
|
}
|
2010-06-15 12:43:46 +08:00
|
|
|
}
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Creates a new graph where if new edges are needed (for cyclic dependency checking) they can be automatically created using a generic MappingFunction.
|
|
|
|
|
* @return A new graph
|
|
|
|
|
*/
|
2010-06-23 02:39:20 +08:00
|
|
|
private def newGraph = new SimpleDirectedGraph[QNode, QFunction](new EdgeFactory[QNode, QFunction] {
|
2010-08-10 00:42:48 +08:00
|
|
|
def createEdge(input: QNode, output: QNode) = new MappingFunction(input.files, output.files)})
|
2010-06-23 02:39:20 +08:00
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Adds a generic QFunction to the graph.
|
|
|
|
|
* @param f Generic QFunction to add to the graph.
|
|
|
|
|
*/
|
2010-06-26 04:51:13 +08:00
|
|
|
private def addFunction(f: QFunction): Unit = {
|
2010-06-23 02:39:20 +08:00
|
|
|
try {
|
|
|
|
|
f match {
|
2010-08-12 05:58:26 +08:00
|
|
|
case cmd: CommandLineFunction => cmd.qSettings = this.qSettings
|
|
|
|
|
case map: MappingFunction => /* do nothing for mapping functions */
|
|
|
|
|
}
|
|
|
|
|
f.freeze
|
|
|
|
|
val inputs = QNode(f.inputs)
|
|
|
|
|
val outputs = QNode(f.outputs)
|
|
|
|
|
val newSource = jobGraph.addVertex(inputs)
|
|
|
|
|
val newTarget = jobGraph.addVertex(outputs)
|
|
|
|
|
val removedEdges = jobGraph.removeAllEdges(inputs, outputs)
|
|
|
|
|
val added = jobGraph.addEdge(inputs, outputs, f)
|
2010-08-13 23:54:08 +08:00
|
|
|
if (this.debugMode) {
|
|
|
|
|
logger.debug("Mapped from: " + inputs)
|
|
|
|
|
logger.debug("Mapped to: " + outputs)
|
|
|
|
|
logger.debug("Mapped via: " + f)
|
|
|
|
|
logger.debug("Removed edges: " + removedEdges)
|
|
|
|
|
logger.debug("New source?: " + newSource)
|
|
|
|
|
logger.debug("New target?: " + newTarget)
|
|
|
|
|
logger.debug("")
|
2010-06-23 02:39:20 +08:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
case e: Exception =>
|
|
|
|
|
throw new QException("Error adding function: " + f, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Checks to see if the set of files has more than one file and if so adds input mappings between the set and the individual files.
|
|
|
|
|
* @param files Set to check.
|
|
|
|
|
*/
|
|
|
|
|
private def addCollectionInputs(files: Set[File]): Unit = {
|
|
|
|
|
if (files.size > 1)
|
|
|
|
|
for (file <- files)
|
|
|
|
|
addMappingEdge(Set(file), files)
|
2010-06-23 02:39:20 +08:00
|
|
|
}
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Checks to see if the set of files has more than one file and if so adds output mappings between the individual files and the set.
|
|
|
|
|
* @param files Set to check.
|
|
|
|
|
*/
|
|
|
|
|
private def addCollectionOutputs(files: Set[File]): Unit = {
|
|
|
|
|
if (files.size > 1)
|
|
|
|
|
for (file <- files)
|
|
|
|
|
addMappingEdge(files, Set(file))
|
2010-06-15 12:43:46 +08:00
|
|
|
}
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Adds a directed graph edge between the input set and the output set if there isn't a direct relationship between the two nodes already.
|
|
|
|
|
* @param input Input set of files.
|
|
|
|
|
* @param output Output set of files.
|
|
|
|
|
*/
|
|
|
|
|
private def addMappingEdge(input: Set[File], output: Set[File]) = {
|
|
|
|
|
val hasEdge = input == output ||
|
|
|
|
|
jobGraph.getEdge(QNode(input), QNode(output)) != null ||
|
|
|
|
|
jobGraph.getEdge(QNode(output), QNode(input)) != null
|
2010-06-26 04:51:13 +08:00
|
|
|
if (!hasEdge)
|
2010-08-10 00:42:48 +08:00
|
|
|
addFunction(new MappingFunction(input, output))
|
2010-06-26 04:51:13 +08:00
|
|
|
}
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Returns true if the edge is an internal mapping edge.
|
|
|
|
|
* @param edge Edge to check.
|
|
|
|
|
* @return true if the edge is an internal mapping edge.
|
|
|
|
|
*/
|
2010-06-23 02:39:20 +08:00
|
|
|
private def isMappingEdge(edge: QFunction) =
|
|
|
|
|
edge.isInstanceOf[MappingFunction]
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Returns true if the edge is mapping edge that is not needed because it does
|
|
|
|
|
* not direct input or output from a user generated CommandLineFunction.
|
|
|
|
|
* @param edge Edge to check.
|
|
|
|
|
* @return true if the edge is not needed in the graph.
|
|
|
|
|
*/
|
2010-06-23 02:39:20 +08:00
|
|
|
private def isFiller(edge: QFunction) = {
|
|
|
|
|
if (isMappingEdge(edge)) {
|
2010-06-26 04:51:13 +08:00
|
|
|
if (jobGraph.outgoingEdgesOf(jobGraph.getEdgeTarget(edge)).size == 0)
|
2010-06-23 02:39:20 +08:00
|
|
|
true
|
2010-06-26 04:51:13 +08:00
|
|
|
else if (jobGraph.incomingEdgesOf(jobGraph.getEdgeSource(edge)).size == 0)
|
2010-06-23 02:39:20 +08:00
|
|
|
true
|
|
|
|
|
else false
|
|
|
|
|
} else false
|
2010-06-15 12:43:46 +08:00
|
|
|
}
|
2010-06-23 02:39:20 +08:00
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Returns true if the node is not connected to any edges.
|
|
|
|
|
* @param node Node (set of files) to check
|
|
|
|
|
* @return true if this set of files is not needed in the graph.
|
|
|
|
|
*/
|
2010-06-23 02:39:20 +08:00
|
|
|
private def isOrphan(node: QNode) =
|
|
|
|
|
(jobGraph.incomingEdgesOf(node).size + jobGraph.outgoingEdgesOf(node).size) == 0
|
2010-07-16 06:32:48 +08:00
|
|
|
|
2010-08-12 05:58:26 +08:00
|
|
|
/**
|
|
|
|
|
* Utility function for looping over the internal graph and running functions.
|
|
|
|
|
* @param edgeFunction Optional function to run for each edge visited.
|
|
|
|
|
* @param nodeFunction Optional function to run for each node visited.
|
|
|
|
|
*/
|
|
|
|
|
private def loop(edgeFunction: PartialFunction[CommandLineFunction, Unit] = null, nodeFunction: PartialFunction[QNode, Unit] = null) = {
|
|
|
|
|
val iterator = new TopologicalOrderIterator(this.jobGraph)
|
|
|
|
|
iterator.addTraversalListener(new TraversalListenerAdapter[QNode, QFunction] {
|
|
|
|
|
override def edgeTraversed(event: EdgeTraversalEvent[QNode, QFunction]) = event.getEdge match {
|
|
|
|
|
case cmd: CommandLineFunction => if (edgeFunction != null && edgeFunction.isDefinedAt(cmd)) edgeFunction(cmd)
|
|
|
|
|
case map: MappingFunction => /* do nothing for mapping functions */
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
iterator.foreach(node => if (nodeFunction != null && nodeFunction.isDefinedAt(node)) nodeFunction(node))
|
|
|
|
|
}
|
|
|
|
|
|
2010-08-10 00:42:48 +08:00
|
|
|
/**
|
|
|
|
|
* Outputs the graph to a .dot file.
|
|
|
|
|
* http://en.wikipedia.org/wiki/DOT_language
|
|
|
|
|
* @param file Path to output the .dot file.
|
|
|
|
|
*/
|
2010-08-12 05:58:26 +08:00
|
|
|
private def renderToDot(file: java.io.File) = {
|
2010-07-16 06:32:48 +08:00
|
|
|
val out = new java.io.FileWriter(file)
|
|
|
|
|
|
|
|
|
|
// todo -- we need a nice way to visualize the key pieces of information about commands. Perhaps a
|
|
|
|
|
// todo -- visualizeString() command, or something that shows inputs / outputs
|
|
|
|
|
val ve = new org.jgrapht.ext.EdgeNameProvider[QFunction] {
|
2010-07-17 04:54:51 +08:00
|
|
|
def getEdgeName( function: QFunction ) = function.dotString
|
2010-07-16 06:32:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//val iterator = new TopologicalOrderIterator(qGraph.jobGraph)
|
|
|
|
|
(new DOTExporter(new org.jgrapht.ext.IntegerNameProvider[QNode](), null, ve)).export(out, jobGraph)
|
|
|
|
|
|
|
|
|
|
out.close
|
|
|
|
|
}
|
2010-06-15 12:43:46 +08:00
|
|
|
}
|