|
1 /* Title: Pure/Admin/build_status.scala |
|
2 Author: Makarius |
|
3 |
|
4 Present recent build status information from database. |
|
5 */ |
|
6 |
|
7 package isabelle |
|
8 |
|
9 |
|
10 object Build_Status |
|
11 { |
|
12 private val default_target_dir = Path.explode("build_status") |
|
13 private val default_history_length = 30 |
|
14 private val default_image_size = (800, 600) |
|
15 |
|
16 |
|
17 /* data profiles */ |
|
18 |
|
19 sealed case class Profile(name: String, sql: String) |
|
20 { |
|
21 def select(columns: List[SQL.Column], days: Int, only_sessions: Set[String]): SQL.Source = |
|
22 { |
|
23 val sql_sessions = |
|
24 if (only_sessions.isEmpty) "" |
|
25 else |
|
26 only_sessions.iterator.map(a => Build_Log.Data.session_name + " = " + SQL.string(a)) |
|
27 .mkString("(", " OR ", ") AND ") |
|
28 |
|
29 Build_Log.Data.universal_table.select(columns, distinct = true, |
|
30 sql = "WHERE " + |
|
31 Build_Log.Data.pull_date + " > " + Build_Log.Data.recent_time(days) + " AND " + |
|
32 Build_Log.Data.status + " = " + SQL.string(Build_Log.Session_Status.finished.toString) + |
|
33 " AND " + sql_sessions + SQL.enclose(sql) + |
|
34 " ORDER BY " + Build_Log.Data.pull_date + " DESC") |
|
35 } |
|
36 } |
|
37 |
|
38 val standard_profiles: List[Profile] = |
|
39 Jenkins.build_log_profiles ::: |
|
40 Isabelle_Cronjob.remote_builds.flatten.toList.map(r => Profile(r.name, r.sql)) |
|
41 |
|
42 sealed case class Entry(date: Date, timing: Timing, ml_timing: Timing) |
|
43 { |
|
44 def check(elapsed_threshold: Time): Boolean = |
|
45 !timing.is_zero && timing.elapsed >= elapsed_threshold |
|
46 } |
|
47 |
|
48 type Data = Map[String, Map[String, List[Entry]]] |
|
49 |
|
50 |
|
51 /* read data */ |
|
52 |
|
53 def read_data(options: Options, |
|
54 profiles: List[Profile] = standard_profiles, |
|
55 progress: Progress = No_Progress, |
|
56 history_length: Int = default_history_length, |
|
57 only_sessions: Set[String] = Set.empty, |
|
58 elapsed_threshold: Time = Time.zero): Data = |
|
59 { |
|
60 var data: Data = Map.empty |
|
61 |
|
62 val store = Build_Log.store(options) |
|
63 using(store.open_database())(db => |
|
64 { |
|
65 for (profile <- profiles) { |
|
66 progress.echo("input " + quote(profile.name)) |
|
67 val columns = |
|
68 List( |
|
69 Build_Log.Data.pull_date, |
|
70 Build_Log.Settings.ML_PLATFORM, |
|
71 Build_Log.Data.session_name, |
|
72 Build_Log.Data.threads, |
|
73 Build_Log.Data.timing_elapsed, |
|
74 Build_Log.Data.timing_cpu, |
|
75 Build_Log.Data.timing_gc, |
|
76 Build_Log.Data.ml_timing_elapsed, |
|
77 Build_Log.Data.ml_timing_cpu, |
|
78 Build_Log.Data.ml_timing_gc) |
|
79 |
|
80 db.using_statement(profile.select(columns, history_length, only_sessions))(stmt => |
|
81 { |
|
82 val res = stmt.execute_query() |
|
83 while (res.next()) { |
|
84 val ml_platform = res.string(Build_Log.Settings.ML_PLATFORM) |
|
85 val threads = res.get_int(Build_Log.Data.threads) |
|
86 val name = |
|
87 profile.name + |
|
88 "_m" + (if (ml_platform.startsWith("x86_64")) "64" else "32") + |
|
89 "_M" + threads.getOrElse(1) |
|
90 |
|
91 val session = res.string(Build_Log.Data.session_name) |
|
92 val entry = |
|
93 Entry(res.date(Build_Log.Data.pull_date), |
|
94 res.timing( |
|
95 Build_Log.Data.timing_elapsed, |
|
96 Build_Log.Data.timing_cpu, |
|
97 Build_Log.Data.timing_gc), |
|
98 res.timing( |
|
99 Build_Log.Data.ml_timing_elapsed, |
|
100 Build_Log.Data.ml_timing_cpu, |
|
101 Build_Log.Data.ml_timing_gc)) |
|
102 |
|
103 val session_entries = data.getOrElse(name, Map.empty) |
|
104 val entries = session_entries.getOrElse(session, Nil) |
|
105 data += (name -> (session_entries + (session -> (entry :: entries)))) |
|
106 } |
|
107 }) |
|
108 } |
|
109 }) |
|
110 |
|
111 for { |
|
112 (name, session_entries) <- data |
|
113 session_entries1 <- |
|
114 { |
|
115 val session_entries1 = |
|
116 for { |
|
117 (session, entries) <- session_entries |
|
118 if entries.filter(_.check(elapsed_threshold)).length >= 3 |
|
119 } yield (session, entries) |
|
120 if (session_entries1.isEmpty) None |
|
121 else Some(session_entries1) |
|
122 } |
|
123 } yield (name, session_entries1) |
|
124 } |
|
125 |
|
126 |
|
127 /* present data */ |
|
128 |
|
129 private val html_header = """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> |
|
130 <html> |
|
131 <head><title>Build status</title></head> |
|
132 <body> |
|
133 """ |
|
134 private val html_footer = """ |
|
135 </body> |
|
136 </html> |
|
137 """ |
|
138 |
|
139 def present_data(data: Data, |
|
140 progress: Progress = No_Progress, |
|
141 target_dir: Path = default_target_dir, |
|
142 image_size: (Int, Int) = default_image_size, |
|
143 ml_timing: Option[Boolean] = None) |
|
144 { |
|
145 val data_entries = data.toList.sortBy(_._1) |
|
146 for ((name, session_entries) <- data_entries) { |
|
147 val dir = target_dir + Path.explode(name) |
|
148 progress.echo("output " + dir) |
|
149 Isabelle_System.mkdirs(dir) |
|
150 |
|
151 for ((session, entries) <- session_entries) { |
|
152 Isabelle_System.with_tmp_file(session, "data") { data_file => |
|
153 Isabelle_System.with_tmp_file(session, "gnuplot") { gnuplot_file => |
|
154 |
|
155 File.write(data_file, |
|
156 cat_lines( |
|
157 entries.map(entry => |
|
158 List(entry.date.unix_epoch.toString, |
|
159 entry.timing.elapsed.minutes, |
|
160 entry.timing.cpu.minutes, |
|
161 entry.ml_timing.elapsed.minutes, |
|
162 entry.ml_timing.cpu.minutes, |
|
163 entry.ml_timing.gc.minutes).mkString(" ")))) |
|
164 |
|
165 val plots1 = |
|
166 List( |
|
167 """ using 1:3 smooth sbezier title "cpu time (smooth)" """, |
|
168 """ using 1:3 smooth csplines title "cpu time" """, |
|
169 """ using 1:2 smooth sbezier title "elapsed time (smooth)" """, |
|
170 """ using 1:2 smooth csplines title "elapsed time" """) |
|
171 val plots2 = |
|
172 List( |
|
173 """ using 1:5 smooth sbezier title "ML cpu time (smooth)" """, |
|
174 """ using 1:5 smooth csplines title "ML cpu time" """, |
|
175 """ using 1:4 smooth sbezier title "ML elapsed time (smooth)" """, |
|
176 """ using 1:4 smooth csplines title "ML elapsed time" """, |
|
177 """ using 1:6 smooth sbezier title "ML gc time (smooth)" """, |
|
178 """ using 1:6 smooth csplines title "ML gc time" """) |
|
179 val plots = |
|
180 ml_timing match { |
|
181 case None => plots1 |
|
182 case Some(false) => plots1 ::: plots2 |
|
183 case Some(true) => plots2 |
|
184 } |
|
185 |
|
186 File.write(gnuplot_file, """ |
|
187 set terminal png size """ + image_size._1 + "," + image_size._2 + """ |
|
188 set output """ + quote(File.standard_path(dir + Path.basic(session + ".png"))) + """ |
|
189 set xdata time |
|
190 set timefmt "%s" |
|
191 set format x "%d-%b" |
|
192 set xlabel """ + quote(session) + """ noenhanced |
|
193 set key left top |
|
194 plot [] [0:] """ + plots.map(s => quote(data_file.implode) + " " + s).mkString(", ") + "\n") |
|
195 |
|
196 val result = |
|
197 Isabelle_System.bash("\"$ISABELLE_GNUPLOT\" " + File.bash_path(gnuplot_file)) |
|
198 if (result.rc != 0) |
|
199 result.error("Gnuplot failed for " + name + "/" + session).check |
|
200 } |
|
201 } |
|
202 } |
|
203 |
|
204 File.write(dir + Path.basic("index.html"), |
|
205 html_header + "\n<h1>" + HTML.output(name) + "</h1>\n" + |
|
206 cat_lines( |
|
207 session_entries.toList.map(_._1).sorted.map(session => |
|
208 """<br/><img src=""" + quote(HTML.output(session + ".png")) + """><br/>""")) + |
|
209 "\n" + html_footer) |
|
210 } |
|
211 |
|
212 File.write(target_dir + Path.basic("index.html"), |
|
213 html_header + "\n<ul>\n" + |
|
214 cat_lines( |
|
215 data_entries.map(_._1).map(name => |
|
216 """<li> <a href=""" + quote(HTML.output(name + "/index.html")) + """>""" + |
|
217 HTML.output(name) + """</a> </li>""")) + |
|
218 "\n</ul>\n" + html_footer) |
|
219 } |
|
220 |
|
221 |
|
222 /* Isabelle tool wrapper */ |
|
223 |
|
224 val isabelle_tool = |
|
225 Isabelle_Tool("build_status", "present recent build status information from database", args => |
|
226 { |
|
227 var target_dir = default_target_dir |
|
228 var ml_timing: Option[Boolean] = None |
|
229 var only_sessions = Set.empty[String] |
|
230 var elapsed_threshold = Time.zero |
|
231 var history_length = default_history_length |
|
232 var options = Options.init() |
|
233 var image_size = default_image_size |
|
234 |
|
235 val getopts = Getopts(""" |
|
236 Usage: isabelle build_status [OPTIONS] |
|
237 |
|
238 Options are: |
|
239 -D DIR target directory (default """ + default_target_dir + """) |
|
240 -M only ML timing |
|
241 -S SESSIONS only given SESSIONS (comma separated) |
|
242 -T THRESHOLD only sessions with elapsed time >= THRESHOLD (minutes) |
|
243 -l LENGTH length of history (default """ + default_history_length + """) |
|
244 -m include ML timing |
|
245 -o OPTION override Isabelle system OPTION (via NAME=VAL or NAME) |
|
246 -s WxH size of PNG image (default """ + image_size._1 + "x" + image_size._2 + """) |
|
247 |
|
248 Present performance statistics from build log database, which is specified |
|
249 via system options build_log_database_host, build_log_database_user etc. |
|
250 """, |
|
251 "D:" -> (arg => target_dir = Path.explode(arg)), |
|
252 "M" -> (_ => ml_timing = Some(true)), |
|
253 "S:" -> (arg => only_sessions = space_explode(',', arg).toSet), |
|
254 "T:" -> (arg => elapsed_threshold = Time.minutes(Value.Double.parse(arg))), |
|
255 "l:" -> (arg => history_length = Value.Int.parse(arg)), |
|
256 "m" -> (_ => ml_timing = Some(false)), |
|
257 "o:" -> (arg => options = options + arg), |
|
258 "s:" -> (arg => |
|
259 space_explode('x', arg).map(Value.Int.parse(_)) match { |
|
260 case List(w, h) if w > 0 && h > 0 => image_size = (w, h) |
|
261 case _ => error("Error bad PNG image size: " + quote(arg)) |
|
262 })) |
|
263 |
|
264 val more_args = getopts(args) |
|
265 if (more_args.nonEmpty) getopts.usage() |
|
266 |
|
267 val progress = new Console_Progress |
|
268 |
|
269 val data = |
|
270 read_data(options, profiles = standard_profiles, progress = progress, |
|
271 history_length = history_length, only_sessions = only_sessions, |
|
272 elapsed_threshold = elapsed_threshold) |
|
273 |
|
274 present_data(data, progress = progress, target_dir = target_dir, |
|
275 image_size = image_size, ml_timing = ml_timing) |
|
276 |
|
277 }, admin = true) |
|
278 } |