src/Pure/Tools/build_schedule.scala
changeset 79181 9d6d559c9fde
parent 79180 229f49204603
child 79182 6202d0ff36b4
equal deleted inserted replaced
79180:229f49204603 79181:9d6d559c9fde
  1047           }
  1047           }
  1048         }
  1048         }
  1049       }
  1049       }
  1050     }
  1050     }
  1051   }
  1051   }
       
  1052 
       
  1053   def write_schedule_graphic(schedule: Schedule, output: Path): Unit = {
       
  1054     import java.awt.geom.{GeneralPath, Rectangle2D}
       
  1055     import java.awt.{BasicStroke, Color, Graphics2D}
       
  1056 
       
  1057     val line_height = isabelle.graphview.Metrics.default.height
       
  1058     val char_width = isabelle.graphview.Metrics.default.char_width
       
  1059     val padding = isabelle.graphview.Metrics.default.space_width
       
  1060     val gap = isabelle.graphview.Metrics.default.gap
       
  1061 
       
  1062     val graph = schedule.graph
       
  1063 
       
  1064     def text_width(text: String): Double = text.length * char_width
       
  1065 
       
  1066     val generator_height = line_height + padding
       
  1067     val hostname_height = generator_height + line_height + padding
       
  1068     def time_height(time: Time): Double = time.seconds
       
  1069     def date_height(date: Date): Double = time_height(date.time - schedule.start.time)
       
  1070 
       
  1071     val hosts = graph.iterator.map(_._2._1).toList.groupBy(_.node_info.hostname)
       
  1072 
       
  1073     def node_width(node: Schedule.Node): Double = 2 * padding + text_width(node.job_name)
       
  1074 
       
  1075     case class Range(start: Double, stop: Double) {
       
  1076       def proper: List[Range] = if (start < stop) List(this) else Nil
       
  1077       def width: Double = stop - start
       
  1078     }
       
  1079 
       
  1080     val rel_node_ranges =
       
  1081       hosts.toList.flatMap { (hostname, nodes) =>
       
  1082         val sorted = nodes.sortBy(node => (node.start.time.ms, node.end.time.ms, node.job_name))
       
  1083         sorted.foldLeft((List.empty[Schedule.Node], Map.empty[Schedule.Node, Range])) {
       
  1084           case ((nodes, allocated), node) =>
       
  1085             val width = node_width(node) + padding
       
  1086             val parallel = nodes.filter(_.end.time > node.start.time)
       
  1087             val (last, slots) =
       
  1088               parallel.sortBy(allocated(_).start).foldLeft((0D, List.empty[Range])) {
       
  1089                 case ((start, ranges), node1) =>
       
  1090                   val node_range = allocated(node1)
       
  1091                   (node_range.stop, ranges ::: Range(start, node_range.start).proper)
       
  1092               }
       
  1093             val start =
       
  1094               (Range(last, Double.MaxValue) :: slots.filter(_.width >= width)).minBy(_.width).start
       
  1095             (node :: parallel, allocated + (node -> Range(start, start + width)))
       
  1096         }._2
       
  1097       }.toMap
       
  1098 
       
  1099     def host_width(hostname: String) =
       
  1100       2 * padding + (hosts(hostname).map(rel_node_ranges(_).stop).max max text_width(hostname))
       
  1101 
       
  1102     def graph_height(graph: Graph[String, Schedule.Node]): Double =
       
  1103       date_height(graph.maximals.map(graph.get_node(_).end).maxBy(_.unix_epoch))
       
  1104 
       
  1105     val height = (hostname_height + 2 * padding + graph_height(graph)).ceil.toInt
       
  1106     val (last, host_starts) =
       
  1107       hosts.keys.foldLeft((0D, Map.empty[String, Double])) {
       
  1108         case ((previous, starts), hostname) =>
       
  1109           (previous + gap + host_width(hostname), starts + (hostname -> previous))
       
  1110       }
       
  1111     val width = (last - gap).ceil.toInt
       
  1112 
       
  1113     def node_start(node: Schedule.Node): Double =
       
  1114       host_starts(node.node_info.hostname) + padding + rel_node_ranges(node).start
       
  1115 
       
  1116     def paint(gfx: Graphics2D): Unit = {
       
  1117       gfx.setColor(Color.LIGHT_GRAY)
       
  1118       gfx.fillRect(0, 0, width, height)
       
  1119       gfx.setRenderingHints(isabelle.graphview.Metrics.rendering_hints)
       
  1120       gfx.setFont(isabelle.graphview.Metrics.default.font)
       
  1121       gfx.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND))
       
  1122 
       
  1123       draw_string(schedule.generator + ", build time: " + schedule.duration.message_hms, padding, 0)
       
  1124 
       
  1125       def draw_host(x: Double, hostname: String): Double = {
       
  1126         val nodes = hosts(hostname).map(_.job_name).toSet
       
  1127         val width = host_width(hostname)
       
  1128         val height = 2 * padding + graph_height(graph.restrict(nodes.contains))
       
  1129         val padding1 = ((width - text_width(hostname)) / 2) max 0
       
  1130         val rect = new Rectangle2D.Double(x, hostname_height, width, height)
       
  1131         gfx.setColor(Color.BLACK)
       
  1132         gfx.draw(rect)
       
  1133         gfx.setColor(Color.GRAY)
       
  1134         gfx.fill(rect)
       
  1135         draw_string(hostname, x + padding1, generator_height)
       
  1136         x + gap + width
       
  1137       }
       
  1138 
       
  1139       def draw_string(str: String, x: Double, y: Double): Unit = {
       
  1140         gfx.setColor(Color.BLACK)
       
  1141         gfx.drawString(str, x.toInt, (y + line_height).toInt)
       
  1142       }
       
  1143 
       
  1144       def node_rect(node: Schedule.Node): Rectangle2D.Double = {
       
  1145         val x = node_start(node)
       
  1146         val y = hostname_height + padding + date_height(node.start)
       
  1147         val width = node_width(node)
       
  1148         val height = time_height(node.duration)
       
  1149         new Rectangle2D.Double(x, y, width, height)
       
  1150       }
       
  1151 
       
  1152       def draw_node(node: Schedule.Node): Rectangle2D.Double = {
       
  1153         val rect = node_rect(node)
       
  1154         gfx.setColor(Color.BLACK)
       
  1155         gfx.draw(rect)
       
  1156         gfx.setColor(Color.WHITE)
       
  1157         gfx.fill(rect)
       
  1158 
       
  1159         def add_text(y: Double, text: String): Double =
       
  1160           if (line_height > rect.height - y || text_width(text) + 2 * padding > rect.width) y
       
  1161           else {
       
  1162             val padding1 = padding min ((rect.height - (y + line_height)) / 2)
       
  1163             draw_string(text, rect.x + padding, rect.y + y + padding1)
       
  1164             y + padding1 + line_height
       
  1165           }
       
  1166 
       
  1167         val node_info = node.node_info
       
  1168 
       
  1169         val duration_str = "(" + node.duration.message_hms + ")"
       
  1170         val node_str =
       
  1171           "on " + proper_string(node_info.toString.stripPrefix(node_info.hostname)).getOrElse("all")
       
  1172         val start_str = "Start: " + (node.start.time - schedule.start.time).message_hms
       
  1173 
       
  1174         List(node.job_name, duration_str, node_str, start_str).foldLeft(0D)(add_text)
       
  1175 
       
  1176         rect
       
  1177       }
       
  1178 
       
  1179       def draw_arrow(from: Schedule.Node, to: Rectangle2D.Double, curve: Double = 10): Unit = {
       
  1180         val from_rect = node_rect(from)
       
  1181 
       
  1182         val path = new GeneralPath()
       
  1183         path.moveTo(from_rect.getCenterX, from_rect.getMaxY)
       
  1184         path.lineTo(to.getCenterX, to.getMinY)
       
  1185 
       
  1186         gfx.setColor(Color.BLUE)
       
  1187         gfx.draw(path)
       
  1188       }
       
  1189 
       
  1190       hosts.keys.foldLeft(0D)(draw_host)
       
  1191 
       
  1192       graph.topological_order.foreach { job_name =>
       
  1193         val node = graph.get_node(job_name)
       
  1194         val rect = draw_node(node)
       
  1195 
       
  1196         graph.imm_preds(job_name).foreach(pred => draw_arrow(graph.get_node(pred), rect))
       
  1197       }
       
  1198     }
       
  1199 
       
  1200     val name = output.file_name
       
  1201     if (File.is_png(name)) Graphics_File.write_png(output.file, paint, width, height)
       
  1202     else if (File.is_pdf(name)) Graphics_File.write_pdf(output.file, paint, width, height)
       
  1203     else error("Bad type of file: " + quote(name) + " (.png or .pdf expected)")
       
  1204   }
  1052 }
  1205 }