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 } |