src/Pure/Admin/build_log.scala
changeset 75393 87ebf5a50283
parent 74782 0a87ea7eb76f
child 75394 42267c650205
equal deleted inserted replaced
75388:b3ca4a6ed74b 75393:87ebf5a50283
    14 import scala.collection.immutable.SortedMap
    14 import scala.collection.immutable.SortedMap
    15 import scala.collection.mutable
    15 import scala.collection.mutable
    16 import scala.util.matching.Regex
    16 import scala.util.matching.Regex
    17 
    17 
    18 
    18 
    19 object Build_Log
    19 object Build_Log {
    20 {
       
    21   /** content **/
    20   /** content **/
    22 
    21 
    23   /* properties */
    22   /* properties */
    24 
    23 
    25   object Prop
    24   object Prop {
    26   {
       
    27     val build_tags = SQL.Column.string("build_tags")  // lines
    25     val build_tags = SQL.Column.string("build_tags")  // lines
    28     val build_args = SQL.Column.string("build_args")  // lines
    26     val build_args = SQL.Column.string("build_args")  // lines
    29     val build_group_id = SQL.Column.string("build_group_id")
    27     val build_group_id = SQL.Column.string("build_group_id")
    30     val build_id = SQL.Column.string("build_id")
    28     val build_id = SQL.Column.string("build_id")
    31     val build_engine = SQL.Column.string("build_engine")
    29     val build_engine = SQL.Column.string("build_engine")
    41   }
    39   }
    42 
    40 
    43 
    41 
    44   /* settings */
    42   /* settings */
    45 
    43 
    46   object Settings
    44   object Settings {
    47   {
       
    48     val ISABELLE_BUILD_OPTIONS = SQL.Column.string("ISABELLE_BUILD_OPTIONS")
    45     val ISABELLE_BUILD_OPTIONS = SQL.Column.string("ISABELLE_BUILD_OPTIONS")
    49     val ML_PLATFORM = SQL.Column.string("ML_PLATFORM")
    46     val ML_PLATFORM = SQL.Column.string("ML_PLATFORM")
    50     val ML_HOME = SQL.Column.string("ML_HOME")
    47     val ML_HOME = SQL.Column.string("ML_HOME")
    51     val ML_SYSTEM = SQL.Column.string("ML_SYSTEM")
    48     val ML_SYSTEM = SQL.Column.string("ML_SYSTEM")
    52     val ML_OPTIONS = SQL.Column.string("ML_OPTIONS")
    49     val ML_OPTIONS = SQL.Column.string("ML_OPTIONS")
    55     val all_settings = ISABELLE_BUILD_OPTIONS :: ml_settings
    52     val all_settings = ISABELLE_BUILD_OPTIONS :: ml_settings
    56 
    53 
    57     type Entry = (String, String)
    54     type Entry = (String, String)
    58     type T = List[Entry]
    55     type T = List[Entry]
    59 
    56 
    60     object Entry
    57     object Entry {
    61     {
       
    62       def unapply(s: String): Option[Entry] =
    58       def unapply(s: String): Option[Entry] =
    63         for { (a, b) <- Properties.Eq.unapply(s) }
    59         for { (a, b) <- Properties.Eq.unapply(s) }
    64         yield (a, Library.perhaps_unquote(b))
    60         yield (a, Library.perhaps_unquote(b))
    65       def getenv(a: String): String =
    61       def getenv(a: String): String =
    66         Properties.Eq(a, quote(Isabelle_System.getenv(a)))
    62         Properties.Eq(a, quote(Isabelle_System.getenv(a)))
    91 
    87 
    92   /** log file **/
    88   /** log file **/
    93 
    89 
    94   def print_date(date: Date): String = Log_File.Date_Format(date)
    90   def print_date(date: Date): String = Log_File.Date_Format(date)
    95 
    91 
    96   object Log_File
    92   object Log_File {
    97   {
       
    98     /* log file */
    93     /* log file */
    99 
    94 
   100     def plain_name(name: String): String =
    95     def plain_name(name: String): String = {
   101     {
       
   102       List(".log", ".log.gz", ".log.xz", ".gz", ".xz").find(name.endsWith) match {
    96       List(".log", ".log.gz", ".log.xz", ".gz", ".xz").find(name.endsWith) match {
   103         case Some(s) => Library.try_unsuffix(s, name).get
    97         case Some(s) => Library.try_unsuffix(s, name).get
   104         case None => name
    98         case None => name
   105       }
    99       }
   106     }
   100     }
   109       new Log_File(plain_name(name), lines.map(Library.trim_line))
   103       new Log_File(plain_name(name), lines.map(Library.trim_line))
   110 
   104 
   111     def apply(name: String, text: String): Log_File =
   105     def apply(name: String, text: String): Log_File =
   112       new Log_File(plain_name(name), Library.trim_split_lines(text))
   106       new Log_File(plain_name(name), Library.trim_split_lines(text))
   113 
   107 
   114     def apply(file: JFile): Log_File =
   108     def apply(file: JFile): Log_File = {
   115     {
       
   116       val name = file.getName
   109       val name = file.getName
   117       val text =
   110       val text =
   118         if (name.endsWith(".gz")) File.read_gzip(file)
   111         if (name.endsWith(".gz")) File.read_gzip(file)
   119         else if (name.endsWith(".xz")) File.read_xz(file)
   112         else if (name.endsWith(".xz")) File.read_xz(file)
   120         else File.read(file)
   113         else File.read(file)
   128 
   121 
   129     def is_log(file: JFile,
   122     def is_log(file: JFile,
   130       prefixes: List[String] =
   123       prefixes: List[String] =
   131         List(Build_History.log_prefix, Identify.log_prefix, Identify.log_prefix2,
   124         List(Build_History.log_prefix, Identify.log_prefix, Identify.log_prefix2,
   132           Isatest.log_prefix, AFP_Test.log_prefix, Jenkins.log_prefix),
   125           Isatest.log_prefix, AFP_Test.log_prefix, Jenkins.log_prefix),
   133       suffixes: List[String] = List(".log", ".log.gz", ".log.xz")): Boolean =
   126       suffixes: List[String] = List(".log", ".log.gz", ".log.xz")
   134     {
   127     ): Boolean = {
   135       val name = file.getName
   128       val name = file.getName
   136 
   129 
   137       prefixes.exists(name.startsWith) &&
   130       prefixes.exists(name.startsWith) &&
   138       suffixes.exists(name.endsWith) &&
   131       suffixes.exists(name.endsWith) &&
   139       name != "isatest.log" &&
   132       name != "isatest.log" &&
   142     }
   135     }
   143 
   136 
   144 
   137 
   145     /* date format */
   138     /* date format */
   146 
   139 
   147     val Date_Format =
   140     val Date_Format = {
   148     {
       
   149       val fmts =
   141       val fmts =
   150         Date.Formatter.variants(
   142         Date.Formatter.variants(
   151           List("EEE MMM d HH:mm:ss O yyyy", "EEE MMM d HH:mm:ss VV yyyy"),
   143           List("EEE MMM d HH:mm:ss O yyyy", "EEE MMM d HH:mm:ss VV yyyy"),
   152           List(Locale.ENGLISH, Locale.GERMAN)) :::
   144           List(Locale.ENGLISH, Locale.GERMAN)) :::
   153         List(
   145         List(
   183 
   175 
   184       Date.Format.make(fmts, tune)
   176       Date.Format.make(fmts, tune)
   185     }
   177     }
   186   }
   178   }
   187 
   179 
   188   class Log_File private(val name: String, val lines: List[String])
   180   class Log_File private(val name: String, val lines: List[String]) {
   189   {
       
   190     log_file =>
   181     log_file =>
   191 
   182 
   192     override def toString: String = name
   183     override def toString: String = name
   193 
   184 
   194     def text: String = cat_lines(lines)
   185     def text: String = cat_lines(lines)
   197       error("Error in log file " + quote(name) + ": " + msg)
   188       error("Error in log file " + quote(name) + ": " + msg)
   198 
   189 
   199 
   190 
   200     /* date format */
   191     /* date format */
   201 
   192 
   202     object Strict_Date
   193     object Strict_Date {
   203     {
       
   204       def unapply(s: String): Some[Date] =
   194       def unapply(s: String): Some[Date] =
   205         try { Some(Log_File.Date_Format.parse(s)) }
   195         try { Some(Log_File.Date_Format.parse(s)) }
   206         catch { case exn: DateTimeParseException => log_file.err(exn.getMessage) }
   196         catch { case exn: DateTimeParseException => log_file.err(exn.getMessage) }
   207     }
   197     }
   208 
   198 
   267 
   257 
   268 
   258 
   269 
   259 
   270   /** digested meta info: produced by Admin/build_history in log.xz file **/
   260   /** digested meta info: produced by Admin/build_history in log.xz file **/
   271 
   261 
   272   object Meta_Info
   262   object Meta_Info {
   273   {
       
   274     val empty: Meta_Info = Meta_Info(Nil, Nil)
   263     val empty: Meta_Info = Meta_Info(Nil, Nil)
   275   }
   264   }
   276 
   265 
   277   sealed case class Meta_Info(props: Properties.T, settings: Settings.T)
   266   sealed case class Meta_Info(props: Properties.T, settings: Settings.T) {
   278   {
       
   279     def is_empty: Boolean = props.isEmpty && settings.isEmpty
   267     def is_empty: Boolean = props.isEmpty && settings.isEmpty
   280 
   268 
   281     def get(c: SQL.Column): Option[String] =
   269     def get(c: SQL.Column): Option[String] =
   282       Properties.get(props, c.name) orElse
   270       Properties.get(props, c.name) orElse
   283       Properties.get(settings, c.name)
   271       Properties.get(settings, c.name)
   284 
   272 
   285     def get_date(c: SQL.Column): Option[Date] =
   273     def get_date(c: SQL.Column): Option[Date] =
   286       get(c).map(Log_File.Date_Format.parse)
   274       get(c).map(Log_File.Date_Format.parse)
   287   }
   275   }
   288 
   276 
   289   object Identify
   277   object Identify {
   290   {
       
   291     val log_prefix = "isabelle_identify_"
   278     val log_prefix = "isabelle_identify_"
   292     val log_prefix2 = "plain_identify_"
   279     val log_prefix2 = "plain_identify_"
   293 
   280 
   294     def engine(log_file: Log_File): String =
   281     def engine(log_file: Log_File): String =
   295       if (log_file.name.startsWith(Jenkins.log_prefix)) "jenkins_identify"
   282       if (log_file.name.startsWith(Jenkins.log_prefix)) "jenkins_identify"
   306     val No_End = new Regex("""$.""")
   293     val No_End = new Regex("""$.""")
   307     val Isabelle_Version = List(new Regex("""^Isabelle version: (\S+)$"""))
   294     val Isabelle_Version = List(new Regex("""^Isabelle version: (\S+)$"""))
   308     val AFP_Version = List(new Regex("""^AFP version: (\S+)$"""))
   295     val AFP_Version = List(new Regex("""^AFP version: (\S+)$"""))
   309   }
   296   }
   310 
   297 
   311   object Isatest
   298   object Isatest {
   312   {
       
   313     val log_prefix = "isatest-makeall-"
   299     val log_prefix = "isatest-makeall-"
   314     val engine = "isatest"
   300     val engine = "isatest"
   315     val Start = new Regex("""^------------------- starting test --- (.+) --- (.+)$""")
   301     val Start = new Regex("""^------------------- starting test --- (.+) --- (.+)$""")
   316     val End = new Regex("""^------------------- test (?:successful|FAILED) --- (.+) --- .*$""")
   302     val End = new Regex("""^------------------- test (?:successful|FAILED) --- (.+) --- .*$""")
   317     val Isabelle_Version = List(new Regex("""^Isabelle version: (\S+)$"""))
   303     val Isabelle_Version = List(new Regex("""^Isabelle version: (\S+)$"""))
   318   }
   304   }
   319 
   305 
   320   object AFP_Test
   306   object AFP_Test {
   321   {
       
   322     val log_prefix = "afp-test-devel-"
   307     val log_prefix = "afp-test-devel-"
   323     val engine = "afp-test"
   308     val engine = "afp-test"
   324     val Start = new Regex("""^Start test(?: for .+)? at ([^,]+), (.*)$""")
   309     val Start = new Regex("""^Start test(?: for .+)? at ([^,]+), (.*)$""")
   325     val Start_Old = new Regex("""^Start test(?: for .+)? at ([^,]+)$""")
   310     val Start_Old = new Regex("""^Start test(?: for .+)? at ([^,]+)$""")
   326     val End = new Regex("""^End test on (.+), .+, elapsed time:.*$""")
   311     val End = new Regex("""^End test on (.+), .+, elapsed time:.*$""")
   327     val Isabelle_Version = List(new Regex("""^Isabelle version: .* -- hg id (\S+)$"""))
   312     val Isabelle_Version = List(new Regex("""^Isabelle version: .* -- hg id (\S+)$"""))
   328     val AFP_Version = List(new Regex("""^AFP version: .* -- hg id (\S+)$"""))
   313     val AFP_Version = List(new Regex("""^AFP version: .* -- hg id (\S+)$"""))
   329     val Bad_Init = new Regex("""^cp:.*: Disc quota exceeded$""")
   314     val Bad_Init = new Regex("""^cp:.*: Disc quota exceeded$""")
   330   }
   315   }
   331 
   316 
   332   object Jenkins
   317   object Jenkins {
   333   {
       
   334     val log_prefix = "jenkins_"
   318     val log_prefix = "jenkins_"
   335     val engine = "jenkins"
   319     val engine = "jenkins"
   336     val Host = new Regex("""^Building remotely on (\S+) \((\S+)\).*$""")
   320     val Host = new Regex("""^Building remotely on (\S+) \((\S+)\).*$""")
   337     val Start = new Regex("""^(?:Started by an SCM change|Started from command line by admin|).*$""")
   321     val Start = new Regex("""^(?:Started by an SCM change|Started from command line by admin|).*$""")
   338     val Start_Date = new Regex("""^Build started at (.+)$""")
   322     val Start_Date = new Regex("""^Build started at (.+)$""")
   346         new Regex("""^ISABELLE_CI_AFP_ID="(\w+)".*$"""))
   330         new Regex("""^ISABELLE_CI_AFP_ID="(\w+)".*$"""))
   347     val CONFIGURATION = "=== CONFIGURATION ==="
   331     val CONFIGURATION = "=== CONFIGURATION ==="
   348     val BUILD = "=== BUILD ==="
   332     val BUILD = "=== BUILD ==="
   349   }
   333   }
   350 
   334 
   351   private def parse_meta_info(log_file: Log_File): Meta_Info =
   335   private def parse_meta_info(log_file: Log_File): Meta_Info = {
   352   {
       
   353     def parse(engine: String, host: String, start: Date,
   336     def parse(engine: String, host: String, start: Date,
   354       End: Regex, Isabelle_Version: List[Regex], AFP_Version: List[Regex]): Meta_Info =
   337       End: Regex, Isabelle_Version: List[Regex], AFP_Version: List[Regex]
   355     {
   338     ): Meta_Info = {
   356       val build_id =
   339       val build_id = {
   357       {
       
   358         val prefix = proper_string(host) orElse proper_string(engine) getOrElse "build"
   340         val prefix = proper_string(host) orElse proper_string(engine) getOrElse "build"
   359         prefix + ":" + start.time.ms
   341         prefix + ":" + start.time.ms
   360       }
   342       }
   361       val build_engine = if (engine == "") Nil else List(Prop.build_engine.name -> engine)
   343       val build_engine = if (engine == "") Nil else List(Prop.build_engine.name -> engine)
   362       val build_host = if (host == "") Nil else List(Prop.build_host.name -> host)
   344       val build_host = if (host == "") Nil else List(Prop.build_host.name -> host)
   424 
   406 
   425   /** build info: toplevel output of isabelle build or Admin/build_history **/
   407   /** build info: toplevel output of isabelle build or Admin/build_history **/
   426 
   408 
   427   val SESSION_NAME = "session_name"
   409   val SESSION_NAME = "session_name"
   428 
   410 
   429   object Session_Status extends Enumeration
   411   object Session_Status extends Enumeration {
   430   {
       
   431     val existing, finished, failed, cancelled = Value
   412     val existing, finished, failed, cancelled = Value
   432   }
   413   }
   433 
   414 
   434   sealed case class Session_Entry(
   415   sealed case class Session_Entry(
   435     chapter: String = "",
   416     chapter: String = "",
   440     sources: Option[String] = None,
   421     sources: Option[String] = None,
   441     heap_size: Option[Long] = None,
   422     heap_size: Option[Long] = None,
   442     status: Option[Session_Status.Value] = None,
   423     status: Option[Session_Status.Value] = None,
   443     errors: List[String] = Nil,
   424     errors: List[String] = Nil,
   444     theory_timings: Map[String, Timing] = Map.empty,
   425     theory_timings: Map[String, Timing] = Map.empty,
   445     ml_statistics: List[Properties.T] = Nil)
   426     ml_statistics: List[Properties.T] = Nil
   446   {
   427   ) {
   447     def proper_groups: Option[String] = if (groups.isEmpty) None else Some(cat_lines(groups))
   428     def proper_groups: Option[String] = if (groups.isEmpty) None else Some(cat_lines(groups))
   448     def finished: Boolean = status == Some(Session_Status.finished)
   429     def finished: Boolean = status == Some(Session_Status.finished)
   449     def failed: Boolean = status == Some(Session_Status.failed)
   430     def failed: Boolean = status == Some(Session_Status.failed)
   450   }
   431   }
   451 
   432 
   452   object Build_Info
   433   object Build_Info {
   453   {
       
   454     val sessions_dummy: Map[String, Session_Entry] =
   434     val sessions_dummy: Map[String, Session_Entry] =
   455       Map("" -> Session_Entry(theory_timings = Map("" -> Timing.zero)))
   435       Map("" -> Session_Entry(theory_timings = Map("" -> Timing.zero)))
   456   }
   436   }
   457 
   437 
   458   sealed case class Build_Info(sessions: Map[String, Session_Entry])
   438   sealed case class Build_Info(sessions: Map[String, Session_Entry]) {
   459   {
       
   460     def finished_sessions: List[String] = for ((a, b) <- sessions.toList if b.finished) yield a
   439     def finished_sessions: List[String] = for ((a, b) <- sessions.toList if b.finished) yield a
   461     def failed_sessions: List[String] = for ((a, b) <- sessions.toList if b.failed) yield a
   440     def failed_sessions: List[String] = for ((a, b) <- sessions.toList if b.failed) yield a
   462   }
   441   }
   463 
   442 
   464   private def parse_build_info(log_file: Log_File, parse_ml_statistics: Boolean): Build_Info =
   443   private def parse_build_info(log_file: Log_File, parse_ml_statistics: Boolean): Build_Info = {
   465   {
   444     object Chapter_Name {
   466     object Chapter_Name
       
   467     {
       
   468       def unapply(s: String): Some[(String, String)] =
   445       def unapply(s: String): Some[(String, String)] =
   469         space_explode('/', s) match {
   446         space_explode('/', s) match {
   470           case List(chapter, name) => Some((chapter, name))
   447           case List(chapter, name) => Some((chapter, name))
   471           case _ => Some(("", s))
   448           case _ => Some(("", s))
   472         }
   449         }
   482       new Regex("""^Timing (\S+) \((\d+) threads, (\d+\.\d+)s elapsed time, (\d+\.\d+)s cpu time, (\d+\.\d+)s GC time.*$""")
   459       new Regex("""^Timing (\S+) \((\d+) threads, (\d+\.\d+)s elapsed time, (\d+\.\d+)s cpu time, (\d+\.\d+)s GC time.*$""")
   483     val Session_Started = new Regex("""^(?:Running|Building) (\S+) \.\.\.$""")
   460     val Session_Started = new Regex("""^(?:Running|Building) (\S+) \.\.\.$""")
   484     val Sources = new Regex("""^Sources (\S+) (\S{""" + SHA1.digest_length + """})$""")
   461     val Sources = new Regex("""^Sources (\S+) (\S{""" + SHA1.digest_length + """})$""")
   485     val Heap = new Regex("""^Heap (\S+) \((\d+) bytes\)$""")
   462     val Heap = new Regex("""^Heap (\S+) \((\d+) bytes\)$""")
   486 
   463 
   487     object Theory_Timing
   464     object Theory_Timing {
   488     {
       
   489       def unapply(line: String): Option[(String, (String, Timing))] =
   465       def unapply(line: String): Option[(String, (String, Timing))] =
   490         Protocol.Theory_Timing_Marker.unapply(line.replace('~', '-')).map(log_file.parse_props)
   466         Protocol.Theory_Timing_Marker.unapply(line.replace('~', '-')).map(log_file.parse_props)
   491         match {
   467         match {
   492           case Some((SESSION_NAME, session) :: props) =>
   468           case Some((SESSION_NAME, session) :: props) =>
   493             for (theory <- Markup.Name.unapply(props))
   469             for (theory <- Markup.Name.unapply(props))
   612     session_timing: Properties.T,
   588     session_timing: Properties.T,
   613     command_timings: List[Properties.T],
   589     command_timings: List[Properties.T],
   614     theory_timings: List[Properties.T],
   590     theory_timings: List[Properties.T],
   615     ml_statistics: List[Properties.T],
   591     ml_statistics: List[Properties.T],
   616     task_statistics: List[Properties.T],
   592     task_statistics: List[Properties.T],
   617     errors: List[String])
   593     errors: List[String]
   618   {
   594   ) {
   619     def error(s: String): Session_Info =
   595     def error(s: String): Session_Info =
   620       copy(errors = errors ::: List(s))
   596       copy(errors = errors ::: List(s))
   621   }
   597   }
   622 
   598 
   623   private def parse_session_info(
   599   private def parse_session_info(
   624     log_file: Log_File,
   600     log_file: Log_File,
   625     command_timings: Boolean,
   601     command_timings: Boolean,
   626     theory_timings: Boolean,
   602     theory_timings: Boolean,
   627     ml_statistics: Boolean,
   603     ml_statistics: Boolean,
   628     task_statistics: Boolean): Session_Info =
   604     task_statistics: Boolean
   629   {
   605   ): Session_Info = {
   630     Session_Info(
   606     Session_Info(
   631       session_timing = log_file.find_props(Protocol.Session_Timing_Marker) getOrElse Nil,
   607       session_timing = log_file.find_props(Protocol.Session_Timing_Marker) getOrElse Nil,
   632       command_timings =
   608       command_timings =
   633         if (command_timings) log_file.filter_props(Protocol.Command_Timing_Marker) else Nil,
   609         if (command_timings) log_file.filter_props(Protocol.Command_Timing_Marker) else Nil,
   634       theory_timings =
   610       theory_timings =
   658 
   634 
   659   /** persistent store **/
   635   /** persistent store **/
   660 
   636 
   661   /* SQL data model */
   637   /* SQL data model */
   662 
   638 
   663   object Data
   639   object Data {
   664   {
       
   665     def build_log_table(name: String, columns: List[SQL.Column], body: String = ""): SQL.Table =
   640     def build_log_table(name: String, columns: List[SQL.Column], body: String = ""): SQL.Table =
   666       SQL.Table("isabelle_build_log_" + name, columns, body)
   641       SQL.Table("isabelle_build_log_" + name, columns, body)
   667 
   642 
   668 
   643 
   669     /* main content */
   644     /* main content */
   710       build_log_table("ml_statistics", List(log_name, session_name, ml_statistics))
   685       build_log_table("ml_statistics", List(log_name, session_name, ml_statistics))
   711 
   686 
   712 
   687 
   713     /* AFP versions */
   688     /* AFP versions */
   714 
   689 
   715     val isabelle_afp_versions_table: SQL.Table =
   690     val isabelle_afp_versions_table: SQL.Table = {
   716     {
       
   717       val version1 = Prop.isabelle_version
   691       val version1 = Prop.isabelle_version
   718       val version2 = Prop.afp_version
   692       val version2 = Prop.afp_version
   719       build_log_table("isabelle_afp_versions", List(version1.make_primary_key, version2),
   693       build_log_table("isabelle_afp_versions", List(version1.make_primary_key, version2),
   720         SQL.select(List(version1, version2), distinct = true) + meta_info_table +
   694         SQL.select(List(version1, version2), distinct = true) + meta_info_table +
   721         " WHERE " + version1.defined + " AND " + version2.defined)
   695         " WHERE " + version1.defined + " AND " + version2.defined)
   726 
   700 
   727     def pull_date(afp: Boolean = false): SQL.Column =
   701     def pull_date(afp: Boolean = false): SQL.Column =
   728       if (afp) SQL.Column.date("afp_pull_date")
   702       if (afp) SQL.Column.date("afp_pull_date")
   729       else SQL.Column.date("pull_date")
   703       else SQL.Column.date("pull_date")
   730 
   704 
   731     def pull_date_table(afp: Boolean = false): SQL.Table =
   705     def pull_date_table(afp: Boolean = false): SQL.Table = {
   732     {
       
   733       val (name, versions) =
   706       val (name, versions) =
   734         if (afp) ("afp_pull_date", List(Prop.isabelle_version, Prop.afp_version))
   707         if (afp) ("afp_pull_date", List(Prop.isabelle_version, Prop.afp_version))
   735         else ("pull_date", List(Prop.isabelle_version))
   708         else ("pull_date", List(Prop.isabelle_version))
   736 
   709 
   737       build_log_table(name, versions.map(_.make_primary_key) ::: List(pull_date(afp)),
   710       build_log_table(name, versions.map(_.make_primary_key) ::: List(pull_date(afp)),
   747 
   720 
   748     def recent_time(days: Int): SQL.Source =
   721     def recent_time(days: Int): SQL.Source =
   749       "now() - INTERVAL '" + days.max(0) + " days'"
   722       "now() - INTERVAL '" + days.max(0) + " days'"
   750 
   723 
   751     def recent_pull_date_table(
   724     def recent_pull_date_table(
   752       days: Int, rev: String = "", afp_rev: Option[String] = None): SQL.Table =
   725       days: Int,
   753     {
   726       rev: String = "",
       
   727       afp_rev: Option[String] = None
       
   728     ): SQL.Table = {
   754       val afp = afp_rev.isDefined
   729       val afp = afp_rev.isDefined
   755       val rev2 = afp_rev.getOrElse("")
   730       val rev2 = afp_rev.getOrElse("")
   756       val table = pull_date_table(afp)
   731       val table = pull_date_table(afp)
   757 
   732 
   758       val version1 = Prop.isabelle_version
   733       val version1 = Prop.isabelle_version
   767            else if (rev == "" && rev2 != "") " OR " + eq2
   742            else if (rev == "" && rev2 != "") " OR " + eq2
   768            else if (rev != "" && rev2 != "") " OR (" + eq1 + " AND " + eq2 + ")"
   743            else if (rev != "" && rev2 != "") " OR (" + eq1 + " AND " + eq2 + ")"
   769            else "")))
   744            else "")))
   770     }
   745     }
   771 
   746 
   772     def select_recent_log_names(days: Int): SQL.Source =
   747     def select_recent_log_names(days: Int): SQL.Source = {
   773     {
       
   774       val table1 = meta_info_table
   748       val table1 = meta_info_table
   775       val table2 = recent_pull_date_table(days)
   749       val table2 = recent_pull_date_table(days)
   776       table1.select(List(log_name), distinct = true) + SQL.join_inner + table2.query_named +
   750       table1.select(List(log_name), distinct = true) + SQL.join_inner + table2.query_named +
   777         " ON " + Prop.isabelle_version(table1) + " = " + Prop.isabelle_version(table2)
   751         " ON " + Prop.isabelle_version(table1) + " = " + Prop.isabelle_version(table2)
   778     }
   752     }
   779 
   753 
   780     def select_recent_versions(days: Int,
   754     def select_recent_versions(
   781       rev: String = "", afp_rev: Option[String] = None, sql: SQL.Source = ""): SQL.Source =
   755       days: Int,
   782     {
   756       rev: String = "",
       
   757       afp_rev: Option[String] = None,
       
   758       sql: SQL.Source = ""
       
   759     ): SQL.Source = {
   783       val afp = afp_rev.isDefined
   760       val afp = afp_rev.isDefined
   784       val version = Prop.isabelle_version
   761       val version = Prop.isabelle_version
   785       val table1 = recent_pull_date_table(days, rev = rev, afp_rev = afp_rev)
   762       val table1 = recent_pull_date_table(days, rev = rev, afp_rev = afp_rev)
   786       val table2 = meta_info_table
   763       val table2 = meta_info_table
   787       val aux_table = SQL.Table("aux", table2.columns, table2.select(sql = sql))
   764       val aux_table = SQL.Table("aux", table2.columns, table2.select(sql = sql))
   796     }
   773     }
   797 
   774 
   798 
   775 
   799     /* universal view on main data */
   776     /* universal view on main data */
   800 
   777 
   801     val universal_table: SQL.Table =
   778     val universal_table: SQL.Table = {
   802     {
       
   803       val afp_pull_date = pull_date(afp = true)
   779       val afp_pull_date = pull_date(afp = true)
   804       val version1 = Prop.isabelle_version
   780       val version1 = Prop.isabelle_version
   805       val version2 = Prop.afp_version
   781       val version2 = Prop.afp_version
   806       val table1 = meta_info_table
   782       val table1 = meta_info_table
   807       val table2 = pull_date_table(afp = true)
   783       val table2 = pull_date_table(afp = true)
   844   /* database access */
   820   /* database access */
   845 
   821 
   846   def store(options: Options, cache: XML.Cache = XML.Cache.make()): Store =
   822   def store(options: Options, cache: XML.Cache = XML.Cache.make()): Store =
   847     new Store(options, cache)
   823     new Store(options, cache)
   848 
   824 
   849   class Store private[Build_Log](options: Options, val cache: XML.Cache)
   825   class Store private[Build_Log](options: Options, val cache: XML.Cache) {
   850   {
       
   851     def open_database(
   826     def open_database(
   852       user: String = options.string("build_log_database_user"),
   827       user: String = options.string("build_log_database_user"),
   853       password: String = options.string("build_log_database_password"),
   828       password: String = options.string("build_log_database_password"),
   854       database: String = options.string("build_log_database_name"),
   829       database: String = options.string("build_log_database_name"),
   855       host: String = options.string("build_log_database_host"),
   830       host: String = options.string("build_log_database_host"),
   856       port: Int = options.int("build_log_database_port"),
   831       port: Int = options.int("build_log_database_port"),
   857       ssh_host: String = options.string("build_log_ssh_host"),
   832       ssh_host: String = options.string("build_log_ssh_host"),
   858       ssh_user: String = options.string("build_log_ssh_user"),
   833       ssh_user: String = options.string("build_log_ssh_user"),
   859       ssh_port: Int = options.int("build_log_ssh_port")): PostgreSQL.Database =
   834       ssh_port: Int = options.int("build_log_ssh_port")
   860     {
   835     ): PostgreSQL.Database = {
   861       PostgreSQL.open_database(
   836       PostgreSQL.open_database(
   862         user = user, password = password, database = database, host = host, port = port,
   837         user = user, password = password, database = database, host = host, port = port,
   863         ssh =
   838         ssh =
   864           if (ssh_host == "") None
   839           if (ssh_host == "") None
   865           else Some(SSH.open_session(options, host = ssh_host, user = ssh_user, port = ssh_port)),
   840           else Some(SSH.open_session(options, host = ssh_host, user = ssh_user, port = ssh_port)),
   866         ssh_close = true)
   841         ssh_close = true)
   867     }
   842     }
   868 
   843 
   869     def update_database(
   844     def update_database(
   870       db: PostgreSQL.Database, dirs: List[Path], ml_statistics: Boolean = false): Unit =
   845       db: PostgreSQL.Database, dirs: List[Path], ml_statistics: Boolean = false): Unit = {
   871     {
       
   872       val log_files =
   846       val log_files =
   873         dirs.flatMap(dir =>
   847         dirs.flatMap(dir =>
   874           File.find_files(dir.file, pred = Log_File.is_log(_), follow_links = true))
   848           File.find_files(dir.file, pred = Log_File.is_log(_), follow_links = true))
   875       write_info(db, log_files, ml_statistics = ml_statistics)
   849       write_info(db, log_files, ml_statistics = ml_statistics)
   876 
   850 
   877       db.create_view(Data.pull_date_table())
   851       db.create_view(Data.pull_date_table())
   878       db.create_view(Data.pull_date_table(afp = true))
   852       db.create_view(Data.pull_date_table(afp = true))
   879       db.create_view(Data.universal_table)
   853       db.create_view(Data.universal_table)
   880     }
   854     }
   881 
   855 
   882     def snapshot_database(db: PostgreSQL.Database, sqlite_database: Path,
   856     def snapshot_database(
   883       days: Int = 100, ml_statistics: Boolean = false): Unit =
   857       db: PostgreSQL.Database,
   884     {
   858       sqlite_database: Path,
       
   859       days: Int = 100,
       
   860       ml_statistics: Boolean = false
       
   861     ): Unit = {
   885       Isabelle_System.make_directory(sqlite_database.dir)
   862       Isabelle_System.make_directory(sqlite_database.dir)
   886       sqlite_database.file.delete
   863       sqlite_database.file.delete
   887 
   864 
   888       using(SQLite.open_database(sqlite_database))(db2 =>
   865       using(SQLite.open_database(sqlite_database))(db2 => {
   889       {
       
   890         db.transaction {
   866         db.transaction {
   891           db2.transaction {
   867           db2.transaction {
   892             // main content
   868             // main content
   893             db2.create_table(Data.meta_info_table)
   869             db2.create_table(Data.meta_info_table)
   894             db2.create_table(Data.sessions_table)
   870             db2.create_table(Data.sessions_table)
   910                   read_build_info(db, log_name, ml_statistics = true))
   886                   read_build_info(db, log_name, ml_statistics = true))
   911               }
   887               }
   912             }
   888             }
   913 
   889 
   914             // pull_date
   890             // pull_date
   915             for (afp <- List(false, true))
   891             for (afp <- List(false, true)) {
   916             {
       
   917               val afp_rev = if (afp) Some("") else None
   892               val afp_rev = if (afp) Some("") else None
   918               val table = Data.pull_date_table(afp)
   893               val table = Data.pull_date_table(afp)
   919               db2.create_table(table)
   894               db2.create_table(table)
   920               db2.using_statement(table.insert())(stmt2 =>
   895               db2.using_statement(table.insert())(stmt2 => {
   921               {
       
   922                 db.using_statement(
   896                 db.using_statement(
   923                   Data.recent_pull_date_table(days, afp_rev = afp_rev).query)(stmt =>
   897                   Data.recent_pull_date_table(days, afp_rev = afp_rev).query)(stmt => {
   924                 {
       
   925                   val res = stmt.execute_query()
   898                   val res = stmt.execute_query()
   926                   while (res.next()) {
   899                   while (res.next()) {
   927                     for ((c, i) <- table.columns.zipWithIndex) {
   900                     for ((c, i) <- table.columns.zipWithIndex) {
   928                       stmt2.string(i + 1) = res.get_string(c)
   901                       stmt2.string(i + 1) = res.get_string(c)
   929                     }
   902                     }
   943 
   916 
   944     def domain(db: SQL.Database, table: SQL.Table, column: SQL.Column): Set[String] =
   917     def domain(db: SQL.Database, table: SQL.Table, column: SQL.Column): Set[String] =
   945       db.using_statement(table.select(List(column), distinct = true))(stmt =>
   918       db.using_statement(table.select(List(column), distinct = true))(stmt =>
   946         stmt.execute_query().iterator(_.string(column)).toSet)
   919         stmt.execute_query().iterator(_.string(column)).toSet)
   947 
   920 
   948     def update_meta_info(db: SQL.Database, log_name: String, meta_info: Meta_Info): Unit =
   921     def update_meta_info(db: SQL.Database, log_name: String, meta_info: Meta_Info): Unit = {
   949     {
       
   950       val table = Data.meta_info_table
   922       val table = Data.meta_info_table
   951       db.using_statement(db.insert_permissive(table))(stmt =>
   923       db.using_statement(db.insert_permissive(table))(stmt => {
   952       {
       
   953         stmt.string(1) = log_name
   924         stmt.string(1) = log_name
   954         for ((c, i) <- table.columns.tail.zipWithIndex) {
   925         for ((c, i) <- table.columns.tail.zipWithIndex) {
   955           if (c.T == SQL.Type.Date)
   926           if (c.T == SQL.Type.Date)
   956             stmt.date(i + 2) = meta_info.get_date(c)
   927             stmt.date(i + 2) = meta_info.get_date(c)
   957           else
   928           else
   959         }
   930         }
   960         stmt.execute()
   931         stmt.execute()
   961       })
   932       })
   962     }
   933     }
   963 
   934 
   964     def update_sessions(db: SQL.Database, log_name: String, build_info: Build_Info): Unit =
   935     def update_sessions(db: SQL.Database, log_name: String, build_info: Build_Info): Unit = {
   965     {
       
   966       val table = Data.sessions_table
   936       val table = Data.sessions_table
   967       db.using_statement(db.insert_permissive(table))(stmt =>
   937       db.using_statement(db.insert_permissive(table))(stmt => {
   968       {
       
   969         val sessions =
   938         val sessions =
   970           if (build_info.sessions.isEmpty) Build_Info.sessions_dummy
   939           if (build_info.sessions.isEmpty) Build_Info.sessions_dummy
   971           else build_info.sessions
   940           else build_info.sessions
   972         for ((session_name, session) <- sessions) {
   941         for ((session_name, session) <- sessions) {
   973           stmt.string(1) = log_name
   942           stmt.string(1) = log_name
   990           stmt.execute()
   959           stmt.execute()
   991         }
   960         }
   992       })
   961       })
   993     }
   962     }
   994 
   963 
   995     def update_theories(db: SQL.Database, log_name: String, build_info: Build_Info): Unit =
   964     def update_theories(db: SQL.Database, log_name: String, build_info: Build_Info): Unit = {
   996     {
       
   997       val table = Data.theories_table
   965       val table = Data.theories_table
   998       db.using_statement(db.insert_permissive(table))(stmt =>
   966       db.using_statement(db.insert_permissive(table))(stmt => {
   999       {
       
  1000         val sessions =
   967         val sessions =
  1001           if (build_info.sessions.forall({ case (_, session) => session.theory_timings.isEmpty }))
   968           if (build_info.sessions.forall({ case (_, session) => session.theory_timings.isEmpty }))
  1002             Build_Info.sessions_dummy
   969             Build_Info.sessions_dummy
  1003           else build_info.sessions
   970           else build_info.sessions
  1004         for {
   971         for {
  1014           stmt.execute()
   981           stmt.execute()
  1015         }
   982         }
  1016       })
   983       })
  1017     }
   984     }
  1018 
   985 
  1019     def update_ml_statistics(db: SQL.Database, log_name: String, build_info: Build_Info): Unit =
   986     def update_ml_statistics(db: SQL.Database, log_name: String, build_info: Build_Info): Unit = {
  1020     {
       
  1021       val table = Data.ml_statistics_table
   987       val table = Data.ml_statistics_table
  1022       db.using_statement(db.insert_permissive(table))(stmt =>
   988       db.using_statement(db.insert_permissive(table))(stmt => {
  1023       {
       
  1024         val ml_stats: List[(String, Option[Bytes])] =
   989         val ml_stats: List[(String, Option[Bytes])] =
  1025           Par_List.map[(String, Session_Entry), (String, Option[Bytes])](
   990           Par_List.map[(String, Session_Entry), (String, Option[Bytes])](
  1026             { case (a, b) => (a, Properties.compress(b.ml_statistics, cache = cache.xz).proper) },
   991             { case (a, b) => (a, Properties.compress(b.ml_statistics, cache = cache.xz).proper) },
  1027             build_info.sessions.iterator.filter(p => p._2.ml_statistics.nonEmpty).toList)
   992             build_info.sessions.iterator.filter(p => p._2.ml_statistics.nonEmpty).toList)
  1028         val entries = if (ml_stats.nonEmpty) ml_stats else List("" -> None)
   993         val entries = if (ml_stats.nonEmpty) ml_stats else List("" -> None)
  1033           stmt.execute()
   998           stmt.execute()
  1034         }
   999         }
  1035       })
  1000       })
  1036     }
  1001     }
  1037 
  1002 
  1038     def write_info(db: SQL.Database, files: List[JFile], ml_statistics: Boolean = false): Unit =
  1003     def write_info(db: SQL.Database, files: List[JFile], ml_statistics: Boolean = false): Unit = {
  1039     {
  1004       abstract class Table_Status(table: SQL.Table) {
  1040       abstract class Table_Status(table: SQL.Table)
       
  1041       {
       
  1042         db.create_table(table)
  1005         db.create_table(table)
  1043         private var known: Set[String] = domain(db, table, Data.log_name)
  1006         private var known: Set[String] = domain(db, table, Data.log_name)
  1044 
  1007 
  1045         def required(file: JFile): Boolean = !known(Log_File.plain_name(file.getName))
  1008         def required(file: JFile): Boolean = !known(Log_File.plain_name(file.getName))
  1046 
  1009 
  1047         def update_db(db: SQL.Database, log_file: Log_File): Unit
  1010         def update_db(db: SQL.Database, log_file: Log_File): Unit
  1048         def update(log_file: Log_File): Unit =
  1011         def update(log_file: Log_File): Unit = {
  1049         {
       
  1050           if (!known(log_file.name)) {
  1012           if (!known(log_file.name)) {
  1051             update_db(db, log_file)
  1013             update_db(db, log_file)
  1052             known += log_file.name
  1014             known += log_file.name
  1053           }
  1015           }
  1054         }
  1016         }
  1075             }
  1037             }
  1076           })
  1038           })
  1077 
  1039 
  1078       for (file_group <-
  1040       for (file_group <-
  1079             files.filter(file => status.exists(_.required(file))).
  1041             files.filter(file => status.exists(_.required(file))).
  1080               grouped(options.int("build_log_transaction_size") max 1))
  1042               grouped(options.int("build_log_transaction_size") max 1)) {
  1081       {
       
  1082         val log_files = Par_List.map[JFile, Log_File](Log_File.apply, file_group)
  1043         val log_files = Par_List.map[JFile, Log_File](Log_File.apply, file_group)
  1083         db.transaction { log_files.foreach(log_file => status.foreach(_.update(log_file))) }
  1044         db.transaction { log_files.foreach(log_file => status.foreach(_.update(log_file))) }
  1084       }
  1045       }
  1085     }
  1046     }
  1086 
  1047 
  1087     def read_meta_info(db: SQL.Database, log_name: String): Option[Meta_Info] =
  1048     def read_meta_info(db: SQL.Database, log_name: String): Option[Meta_Info] = {
  1088     {
       
  1089       val table = Data.meta_info_table
  1049       val table = Data.meta_info_table
  1090       val columns = table.columns.tail
  1050       val columns = table.columns.tail
  1091       db.using_statement(table.select(columns, Data.log_name.where_equal(log_name)))(stmt =>
  1051       db.using_statement(table.select(columns, Data.log_name.where_equal(log_name)))(stmt => {
  1092       {
       
  1093         val res = stmt.execute_query()
  1052         val res = stmt.execute_query()
  1094         if (!res.next()) None
  1053         if (!res.next()) None
  1095         else {
  1054         else {
  1096           val results =
  1055           val results =
  1097             columns.map(c => c.name ->
  1056             columns.map(c => c.name ->
  1109 
  1068 
  1110     def read_build_info(
  1069     def read_build_info(
  1111       db: SQL.Database,
  1070       db: SQL.Database,
  1112       log_name: String,
  1071       log_name: String,
  1113       session_names: List[String] = Nil,
  1072       session_names: List[String] = Nil,
  1114       ml_statistics: Boolean = false): Build_Info =
  1073       ml_statistics: Boolean = false): Build_Info = {
  1115     {
       
  1116       val table1 = Data.sessions_table
  1074       val table1 = Data.sessions_table
  1117       val table2 = Data.ml_statistics_table
  1075       val table2 = Data.ml_statistics_table
  1118 
  1076 
  1119       val where_log_name =
  1077       val where_log_name =
  1120         Data.log_name(table1).where_equal(log_name) + " AND " +
  1078         Data.log_name(table1).where_equal(log_name) + " AND " +
  1134           (columns, SQL.enclose(join))
  1092           (columns, SQL.enclose(join))
  1135         }
  1093         }
  1136         else (columns1, table1.ident)
  1094         else (columns1, table1.ident)
  1137 
  1095 
  1138       val sessions =
  1096       val sessions =
  1139         db.using_statement(SQL.select(columns) + from + " " + where)(stmt =>
  1097         db.using_statement(SQL.select(columns) + from + " " + where)(stmt => {
  1140         {
  1098           stmt.execute_query().iterator(res => {
  1141           stmt.execute_query().iterator(res =>
       
  1142           {
       
  1143             val session_name = res.string(Data.session_name)
  1099             val session_name = res.string(Data.session_name)
  1144             val session_entry =
  1100             val session_entry =
  1145               Session_Entry(
  1101               Session_Entry(
  1146                 chapter = res.string(Data.chapter),
  1102                 chapter = res.string(Data.chapter),
  1147                 groups = split_lines(res.string(Data.groups)),
  1103                 groups = split_lines(res.string(Data.groups)),