src/Pure/Admin/build_stats.scala
changeset 64161 2b1128e95dfb
parent 64106 b7ff61d50b19
child 64220 e7cbf81ec4b7
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/src/Pure/Admin/build_stats.scala	Wed Oct 12 10:22:34 2016 +0200
     1.3 @@ -0,0 +1,191 @@
     1.4 +/*  Title:      Pure/Admin/build_stats.scala
     1.5 +    Author:     Makarius
     1.6 +
     1.7 +Statistics from session build output.
     1.8 +*/
     1.9 +
    1.10 +package isabelle
    1.11 +
    1.12 +
    1.13 +object Build_Stats
    1.14 +{
    1.15 +  /* presentation */
    1.16 +
    1.17 +  private val default_history_length = 100
    1.18 +  private val default_size = (800, 600)
    1.19 +  private val default_only_sessions = Set.empty[String]
    1.20 +  private val default_elapsed_threshold = Time.zero
    1.21 +  private val default_ml_timing: Option[Boolean] = None
    1.22 +
    1.23 +  def present_job(job: String, dir: Path,
    1.24 +    history_length: Int = default_history_length,
    1.25 +    size: (Int, Int) = default_size,
    1.26 +    only_sessions: Set[String] = default_only_sessions,
    1.27 +    elapsed_threshold: Time = default_elapsed_threshold,
    1.28 +    ml_timing: Option[Boolean] = default_ml_timing): List[String] =
    1.29 +  {
    1.30 +    val job_infos = CI_API.build_job_builds(job).sortBy(_.timestamp).reverse.take(history_length)
    1.31 +    if (job_infos.isEmpty) error("No build infos for job " + quote(job))
    1.32 +
    1.33 +    val all_infos =
    1.34 +      Par_List.map((job_info: CI_API.Job_Info) =>
    1.35 +        (job_info.timestamp / 1000, job_info.read_main_log.parse_build_info), job_infos)
    1.36 +    val all_sessions =
    1.37 +      (Set.empty[String] /: all_infos)(
    1.38 +        { case (s, (_, info)) => s ++ info.sessions.keySet })
    1.39 +
    1.40 +    def check_threshold(info: Build_Log.Build_Info, session: String): Boolean =
    1.41 +    {
    1.42 +      val t = info.timing(session)
    1.43 +      !t.is_zero && t.elapsed >= elapsed_threshold
    1.44 +    }
    1.45 +
    1.46 +    val sessions =
    1.47 +      for {
    1.48 +        session <- (if (only_sessions.isEmpty) all_sessions else all_sessions & only_sessions)
    1.49 +        if all_infos.filter({ case (_, info) => check_threshold(info, session) }).length >= 3
    1.50 +      } yield session
    1.51 +
    1.52 +    Isabelle_System.mkdirs(dir)
    1.53 +    for (session <- sessions) {
    1.54 +      Isabelle_System.with_tmp_file(session, "png") { data_file =>
    1.55 +        Isabelle_System.with_tmp_file(session, "gnuplot") { plot_file =>
    1.56 +          val data =
    1.57 +            for { (t, info) <- all_infos if info.finished(session) }
    1.58 +            yield {
    1.59 +              val timing1 = info.timing(session)
    1.60 +              val timing2 = info.ml_timing(session)
    1.61 +              List(t.toString,
    1.62 +                timing1.elapsed.minutes,
    1.63 +                timing1.cpu.minutes,
    1.64 +                timing2.elapsed.minutes,
    1.65 +                timing2.cpu.minutes,
    1.66 +                timing2.gc.minutes).mkString(" ")
    1.67 +            }
    1.68 +          File.write(data_file, cat_lines(data))
    1.69 +
    1.70 +          val plots1 =
    1.71 +            List(
    1.72 +              """ using 1:3 smooth sbezier title "cpu time (smooth)" """,
    1.73 +              """ using 1:3 smooth csplines title "cpu time" """,
    1.74 +              """ using 1:2 smooth sbezier title "elapsed time (smooth)" """,
    1.75 +              """ using 1:2 smooth csplines title "elapsed time" """)
    1.76 +          val plots2 =
    1.77 +            List(
    1.78 +              """ using 1:5 smooth sbezier title "ML cpu time (smooth)" """,
    1.79 +              """ using 1:5 smooth csplines title "ML cpu time" """,
    1.80 +              """ using 1:4 smooth sbezier title "ML elapsed time (smooth)" """,
    1.81 +              """ using 1:4 smooth csplines title "ML elapsed time" """,
    1.82 +              """ using 1:6 smooth sbezier title "ML gc time (smooth)" """,
    1.83 +              """ using 1:6 smooth csplines title "ML gc time" """)
    1.84 +          val plots =
    1.85 +            ml_timing match {
    1.86 +              case None => plots1
    1.87 +              case Some(false) => plots1 ::: plots2
    1.88 +              case Some(true) => plots2
    1.89 +            }
    1.90 +
    1.91 +          val data_file_name = File.standard_path(data_file.getAbsolutePath)
    1.92 +          File.write(plot_file, """
    1.93 +set terminal png size """ + size._1 + "," + size._2 + """
    1.94 +set output """ + quote(File.standard_path(dir + Path.basic(session + ".png"))) + """
    1.95 +set xdata time
    1.96 +set timefmt "%s"
    1.97 +set format x "%d-%b"
    1.98 +set xlabel """ + quote(session) + """ noenhanced
    1.99 +set key left top
   1.100 +plot [] [0:] """ + plots.map(s => quote(data_file_name) + " " + s).mkString(", ") + "\n")
   1.101 +          val result = Isabelle_System.bash("\"$ISABELLE_GNUPLOT\" " + File.bash_path(plot_file))
   1.102 +          if (result.rc != 0) {
   1.103 +            Output.error_message("Session " + session + ": gnuplot error")
   1.104 +            result.print
   1.105 +          }
   1.106 +        }
   1.107 +      }
   1.108 +    }
   1.109 +
   1.110 +    sessions.toList.sorted
   1.111 +  }
   1.112 +
   1.113 +
   1.114 +  /* Isabelle tool wrapper */
   1.115 +
   1.116 +  private val html_header = """<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
   1.117 +<html>
   1.118 +<head><title>Performance statistics from session build output</title></head>
   1.119 +<body>
   1.120 +"""
   1.121 +  private val html_footer = """
   1.122 +</body>
   1.123 +</html>
   1.124 +"""
   1.125 +
   1.126 +  val isabelle_tool =
   1.127 +    Isabelle_Tool("build_stats", "present statistics from session build output", args =>
   1.128 +    {
   1.129 +      var target_dir = Path.explode("stats")
   1.130 +      var ml_timing = default_ml_timing
   1.131 +      var only_sessions = default_only_sessions
   1.132 +      var elapsed_threshold = default_elapsed_threshold
   1.133 +      var history_length = default_history_length
   1.134 +      var size = default_size
   1.135 +
   1.136 +      val getopts = Getopts("""
   1.137 +Usage: isabelle build_stats [OPTIONS] [JOBS ...]
   1.138 +
   1.139 +  Options are:
   1.140 +    -D DIR       target directory (default "stats")
   1.141 +    -M           only ML timing
   1.142 +    -S SESSIONS  only given SESSIONS (comma separated)
   1.143 +    -T THRESHOLD only sessions with elapsed time >= THRESHOLD (minutes)
   1.144 +    -l LENGTH    length of history (default 100)
   1.145 +    -m           include ML timing
   1.146 +    -s WxH       size of PNG image (default 800x600)
   1.147 +
   1.148 +  Present statistics from session build output of the given JOBS, from Jenkins
   1.149 +  continuous build service specified as URL via ISABELLE_JENKINS_ROOT.
   1.150 +""",
   1.151 +        "D:" -> (arg => target_dir = Path.explode(arg)),
   1.152 +        "M" -> (_ => ml_timing = Some(true)),
   1.153 +        "S:" -> (arg => only_sessions = space_explode(',', arg).toSet),
   1.154 +        "T:" -> (arg => elapsed_threshold = Time.minutes(Value.Double.parse(arg))),
   1.155 +        "l:" -> (arg => history_length = Value.Int.parse(arg)),
   1.156 +        "m" -> (_ => ml_timing = Some(false)),
   1.157 +        "s:" -> (arg =>
   1.158 +          space_explode('x', arg).map(Value.Int.parse(_)) match {
   1.159 +            case List(w, h) if w > 0 && h > 0 => size = (w, h)
   1.160 +            case _ => error("Error bad PNG image size: " + quote(arg))
   1.161 +          }))
   1.162 +
   1.163 +      val jobs = getopts(args)
   1.164 +      val all_jobs = CI_API.build_jobs()
   1.165 +      val bad_jobs = jobs.filterNot(all_jobs.contains(_)).sorted
   1.166 +
   1.167 +      if (jobs.isEmpty)
   1.168 +        error("No build jobs given. Available jobs: " + all_jobs.sorted.mkString(" "))
   1.169 +
   1.170 +      if (bad_jobs.nonEmpty)
   1.171 +        error("Unknown build jobs: " + bad_jobs.mkString(" ") +
   1.172 +          "\nAvailable jobs: " + all_jobs.sorted.mkString(" "))
   1.173 +
   1.174 +      for (job <- jobs) {
   1.175 +        val dir = target_dir + Path.basic(job)
   1.176 +        Output.writeln(dir.implode)
   1.177 +        val sessions =
   1.178 +          present_job(job, dir, history_length, size, only_sessions, elapsed_threshold, ml_timing)
   1.179 +        File.write(dir + Path.basic("index.html"),
   1.180 +          html_header + "\n<h1>" + HTML.output(job) + "</h1>\n" +
   1.181 +          cat_lines(
   1.182 +            sessions.map(session =>
   1.183 +              """<br/><img src=""" + quote(HTML.output(session + ".png")) + """><br/>""")) +
   1.184 +          "\n" + html_footer)
   1.185 +      }
   1.186 +
   1.187 +      File.write(target_dir + Path.basic("index.html"),
   1.188 +        html_header + "\n<ul>\n" +
   1.189 +        cat_lines(
   1.190 +          jobs.map(job => """<li> <a href=""" + quote(HTML.output(job + "/index.html")) + """>""" +
   1.191 +            HTML.output(job) + """</a> </li>""")) +
   1.192 +        "\n</ul>\n" + html_footer)
   1.193 +  }, admin = true)
   1.194 +}