Garmin split CSV downloads are used to investigate changes in speed across workouts. Looking for improvements to average 100 meter time.

Garmin Connect application tracks activities by activity type. Activity types include run, bike, cardio, strength. A workout can be created for an activity type. The workout comprises a set of steps. Each step has a time. For example, a run can contain steps of type warmup, run, recover, cool down. Step can be defined by time and distance. A workout is available for execution. The data for each workout is captured. Garmin provides a splits CSV for an executed and recorded instance of a workout. The splits capture the statistics of each step. It is the splits CSV for defined workouts that are processed here. Specifically, the workouts are run activities. The workout definitions vary, but generally the steps are warmup, sets of run and recover, and cool down. Investigated here are changes in run speeds through time as the workouts are executed. Am I getting faster or slower?

The data model Garmin uses for the exports has some deficiencies that have to be worked around. First is that the activity types cannot be subtyped. I can filter by run, but I can’t filter on workouts within a run type. This can be satisfactorily worked around by a naming convention for workouts: Prefix the name of a workout by “workout”. Garmin provides an activity search, so searching on the prefix finds what I want.

Second is that the date of the activity is not part of the downloaded data. Ideally, the CSV would include step date as a column. Alternatively, the activity start date would be part of the automatic naming of the exported CSV, making it available for parsing. I set the activity date in the name manually when I save the downloaded file. A nuisance, but workable.

Most egregiously, there is no step type column in the CSV. Without that, I infer interest based on some other column. In this exploration, I use a step’s average 100 meter per second rate to filter out anything over 30 seconds (see why I am interested in improving?).

I have two goals for this exercise. First is to be able to see changes in speed through time. Second is to beat off R rust. Third is to stay in tune with Tidyverse, which adds to the beauty of R.

I have (technically) unnessarily prefixed functions with package name. Though the script doesn’t care, the prefix reminds me of where the functions are coming from.

library(tidyverse, quietly=TRUE)
library(lubridate, quietly=TRUE)
library(knitr, quietly=TRUE)
library(DT, quietly=TRUE)

Select and combine files to create untidy dataset

sourcefiles <- choose.files()

untidy <- 
  sourcefiles %>% 
  purrr::set_names(nm=basename(.)) %>%
  purrr::map_dfr(readr::read_csv, 
                 .id="source", 
                 col_types=cols(Time=col_character(),
                                'Cumulative Time'=col_character(),
                                'Moving Time'=col_character()
                                )
                 )

Explanation of the code. The function choose.files returns a list of file names selected by the user. The file names are used to read in and stitch together the chosed files. The assumption is that the files are all of the same shape as defined by Garmin for the splits export. We optimistically assume the path is always happy. ### The vector of full source file names chosen by the user

  sourcefiles
[1] "C:\\Data\\downloadeddata\\running\\activity_2020-06-24.csv"
[2] "C:\\Data\\downloadeddata\\running\\activity_2020-07-01.csv"
[3] "C:\\Data\\downloadeddata\\running\\activity_2020-07-08.csv"

The vector’s basename values to be used to create a new column in the data

  sourcefiles %>% basename()
[1] "activity_2020-06-24.csv" "activity_2020-07-01.csv" "activity_2020-07-08.csv"

Note the %>%. This is a pipe operator from the magrittr package, part of the tidyverse. The package offers a set of operators that make code more readable. In this case, sourcefiles (the vector of file names chosen by the user) is piped as the first argument into the function basename. The results of basename are available to feed forward to another function, depicting a pipeline of processing.

The vector’s basename values added to create a named vector.

  sourcefiles %>% purrr::set_names(nm=basename(.))
                                     activity_2020-06-24.csv 
"C:\\Data\\downloadeddata\\running\\activity_2020-06-24.csv" 
                                     activity_2020-07-01.csv 
"C:\\Data\\downloadeddata\\running\\activity_2020-07-01.csv" 
                                     activity_2020-07-08.csv 
"C:\\Data\\downloadeddata\\running\\activity_2020-07-08.csv" 

The function purrr::set_names adds names to the vector from sourcefiles (the strings representing the selected files). The names added to the vector are those returned by basename, which reuses the argument piped to set_names (depicted by “.”).

The entire pipeline that creates the untidy data

untidy <- 
  sourcefiles %>% 
  purrr::set_names(nm=basename(.)) %>%
  purrr::map_dfr(readr::read_csv, 
                 .id="source", 
                 col_types=cols(Time=col_character(),
                                'Cumulative Time'=col_character(),
                                'Moving Time'=col_character()
                                )
                 )

The items of the named vector are mapped to the read_csv function. A new column named “source” is added to the data frame as the process executes. Since the input is a named vector, the values for the new column are taken from the vector’s names. The full file name is used to identify the file to read_csv.

The mapping function adds the col_types stanza as an argument to the read_csv function. The stanza tells read_csv to handle specific CSV columns in a specific manner. Named columns are set to be treated as plain text rather than inferring their type from data and transforming the values as they are read.

Untidy data

DT::datatable(untidy)

Tidy the data and derive new columns

tidied <- untidy[untidy$Laps != "Summary", ]

tidied$Seconds <- lubridate::period_to_seconds(ms(tidied$Time) )

tidied$Meters <- tidied$Distance * 1000.0

tidied$dash100msecs <- round(tidied$Seconds / tidied$Meters * 100, 1)

interesting <- tidied %>% dplyr::select(source, Laps, Distance, Time, Meters, Seconds, dash100msecs)

New columns are derived from the input and added to the data frame. The interesting bits are pulled out.

Display tidy data

min(interesting$dash100msecs)

[1] 22.8

DT::datatable(interesting)

Plot distribution of times

p <- ggplot(tidied[tidied$dash100msecs < 30, ], 
            aes(source, dash100msecs, label=dash100msecs))
p + geom_label(nudge_x = 0.2) + 
    geom_point(aes(colour = factor(Meters)))

LS0tDQp0aXRsZTogIlByb2Nlc3NpbmcgR2FybWluIFNwbGl0IERhdGEiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpHYXJtaW4gc3BsaXQgQ1NWIGRvd25sb2FkcyBhcmUgdXNlZCB0byBpbnZlc3RpZ2F0ZSBjaGFuZ2VzIGluIHNwZWVkIGFjcm9zcyB3b3Jrb3V0cy4gTG9va2luZyBmb3IgaW1wcm92ZW1lbnRzIHRvIGF2ZXJhZ2UgMTAwIG1ldGVyIHRpbWUuDQoNCkdhcm1pbiBDb25uZWN0IGFwcGxpY2F0aW9uIHRyYWNrcyBhY3Rpdml0aWVzIGJ5IGFjdGl2aXR5IHR5cGUuICBBY3Rpdml0eSB0eXBlcyBpbmNsdWRlIHJ1biwgYmlrZSwgY2FyZGlvLCBzdHJlbmd0aC4gIEEgd29ya291dCBjYW4gYmUgY3JlYXRlZCBmb3IgYW4gYWN0aXZpdHkgdHlwZS4gIFRoZSB3b3Jrb3V0IGNvbXByaXNlcyBhIHNldCBvZiBzdGVwcy4gIEVhY2ggc3RlcCBoYXMgYSB0aW1lLiAgRm9yIGV4YW1wbGUsIGEgcnVuIGNhbiBjb250YWluIHN0ZXBzIG9mIHR5cGUgd2FybXVwLCBydW4sIHJlY292ZXIsIGNvb2wgZG93bi4gIFN0ZXAgY2FuIGJlIGRlZmluZWQgYnkgdGltZSBhbmQgZGlzdGFuY2UuICBBIHdvcmtvdXQgaXMgYXZhaWxhYmxlIGZvciBleGVjdXRpb24uICBUaGUgZGF0YSBmb3IgZWFjaCB3b3Jrb3V0IGlzIGNhcHR1cmVkLiAgR2FybWluIHByb3ZpZGVzIGEgc3BsaXRzIENTViBmb3IgYW4gZXhlY3V0ZWQgYW5kIHJlY29yZGVkIGluc3RhbmNlIG9mIGEgd29ya291dC4gIFRoZSBzcGxpdHMgY2FwdHVyZSB0aGUgc3RhdGlzdGljcyBvZiBlYWNoIHN0ZXAuIEl0IGlzIHRoZSBzcGxpdHMgQ1NWIGZvciBkZWZpbmVkIHdvcmtvdXRzIHRoYXQgYXJlIHByb2Nlc3NlZCBoZXJlLiAgU3BlY2lmaWNhbGx5LCB0aGUgd29ya291dHMgYXJlIHJ1biBhY3Rpdml0aWVzLiAgVGhlIHdvcmtvdXQgZGVmaW5pdGlvbnMgdmFyeSwgYnV0IGdlbmVyYWxseSB0aGUgc3RlcHMgYXJlIHdhcm11cCwgc2V0cyBvZiBydW4gYW5kIHJlY292ZXIsIGFuZCBjb29sIGRvd24uICBJbnZlc3RpZ2F0ZWQgaGVyZSBhcmUgY2hhbmdlcyBpbiBydW4gc3BlZWRzIHRocm91Z2ggdGltZSBhcyB0aGUgd29ya291dHMgYXJlIGV4ZWN1dGVkLiAgQW0gSSBnZXR0aW5nIGZhc3RlciBvciBzbG93ZXI/DQoNClRoZSBkYXRhIG1vZGVsIEdhcm1pbiB1c2VzIGZvciB0aGUgZXhwb3J0cyBoYXMgc29tZSBkZWZpY2llbmNpZXMgdGhhdCBoYXZlIHRvIGJlIHdvcmtlZCBhcm91bmQuIEZpcnN0IGlzIHRoYXQgdGhlIGFjdGl2aXR5IHR5cGVzIGNhbm5vdCBiZSBzdWJ0eXBlZC4gIEkgY2FuIGZpbHRlciBieSBydW4sIGJ1dCBJIGNhbid0IGZpbHRlciBvbiB3b3Jrb3V0cyB3aXRoaW4gYSBydW4gdHlwZS4gIFRoaXMgY2FuIGJlIHNhdGlzZmFjdG9yaWx5IHdvcmtlZCBhcm91bmQgYnkgYSBuYW1pbmcgY29udmVudGlvbiBmb3Igd29ya291dHM6IFByZWZpeCB0aGUgbmFtZSBvZiBhIHdvcmtvdXQgYnkgIndvcmtvdXQiLiAgR2FybWluIHByb3ZpZGVzIGFuIGFjdGl2aXR5IHNlYXJjaCwgc28gc2VhcmNoaW5nIG9uIHRoZSBwcmVmaXggZmluZHMgd2hhdCBJIHdhbnQuDQoNClNlY29uZCBpcyB0aGF0IHRoZSBkYXRlIG9mIHRoZSBhY3Rpdml0eSBpcyBub3QgcGFydCBvZiB0aGUgZG93bmxvYWRlZCBkYXRhLiAgSWRlYWxseSwgdGhlIENTViB3b3VsZCBpbmNsdWRlIHN0ZXAgZGF0ZSBhcyBhIGNvbHVtbi4gIEFsdGVybmF0aXZlbHksIHRoZSBhY3Rpdml0eSBzdGFydCBkYXRlIHdvdWxkIGJlIHBhcnQgb2YgdGhlIGF1dG9tYXRpYyBuYW1pbmcgb2YgdGhlIGV4cG9ydGVkIENTViwgbWFraW5nIGl0IGF2YWlsYWJsZSBmb3IgcGFyc2luZy4gIEkgc2V0IHRoZSBhY3Rpdml0eSBkYXRlIGluIHRoZSBuYW1lIG1hbnVhbGx5IHdoZW4gSSBzYXZlIHRoZSBkb3dubG9hZGVkIGZpbGUuICBBIG51aXNhbmNlLCBidXQgd29ya2FibGUuDQoNCk1vc3QgZWdyZWdpb3VzbHksIHRoZXJlIGlzIG5vIHN0ZXAgdHlwZSBjb2x1bW4gaW4gdGhlIENTVi4gIFdpdGhvdXQgdGhhdCwgSSBpbmZlciBpbnRlcmVzdCBiYXNlZCBvbiBzb21lIG90aGVyIGNvbHVtbi4gIEluIHRoaXMgZXhwbG9yYXRpb24sIEkgdXNlIGEgc3RlcCdzIGF2ZXJhZ2UgMTAwIG1ldGVyIHBlciBzZWNvbmQgcmF0ZSB0byBmaWx0ZXIgb3V0IGFueXRoaW5nIG92ZXIgMzAgc2Vjb25kcyAoc2VlIHdoeSBJIGFtIGludGVyZXN0ZWQgaW4gaW1wcm92aW5nPykuDQoNCkkgaGF2ZSB0d28gZ29hbHMgZm9yIHRoaXMgZXhlcmNpc2UuICBGaXJzdCBpcyB0byBiZSBhYmxlIHRvIHNlZSBjaGFuZ2VzIGluIHNwZWVkIHRocm91Z2ggdGltZS4gIFNlY29uZCBpcyB0byBiZWF0IG9mZiBSIHJ1c3QuICBUaGlyZCBpcyB0byBzdGF5IGluIHR1bmUgd2l0aCBUaWR5dmVyc2UsIHdoaWNoIGFkZHMgdG8gdGhlIGJlYXV0eSBvZiBSLg0KDQpJIGhhdmUgKHRlY2huaWNhbGx5KSB1bm5lc3NhcmlseSBwcmVmaXhlZCBmdW5jdGlvbnMgd2l0aCBwYWNrYWdlIG5hbWUuICBUaG91Z2ggdGhlIHNjcmlwdCBkb2Vzbid0IGNhcmUsIHRoZSBwcmVmaXggcmVtaW5kcyBtZSBvZiB3aGVyZSB0aGUgZnVuY3Rpb25zIGFyZSBjb21pbmcgZnJvbS4NCg0KYGBge3J9DQpsaWJyYXJ5KHRpZHl2ZXJzZSwgcXVpZXRseT1UUlVFKQ0KbGlicmFyeShsdWJyaWRhdGUsIHF1aWV0bHk9VFJVRSkNCmxpYnJhcnkoa25pdHIsIHF1aWV0bHk9VFJVRSkNCmxpYnJhcnkoRFQsIHF1aWV0bHk9VFJVRSkNCg0KYGBgDQoNCiMjIFNlbGVjdCBhbmQgY29tYmluZSBmaWxlcyB0byBjcmVhdGUgdW50aWR5IGRhdGFzZXQNCg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0Kc291cmNlZmlsZXMgPC0gY2hvb3NlLmZpbGVzKCkNCg0KdW50aWR5IDwtIA0KICBzb3VyY2VmaWxlcyAlPiUgDQogIHB1cnJyOjpzZXRfbmFtZXMobm09YmFzZW5hbWUoLikpICU+JQ0KICBwdXJycjo6bWFwX2RmcihyZWFkcjo6cmVhZF9jc3YsIA0KICAgICAgICAgICAgICAgICAuaWQ9InNvdXJjZSIsIA0KICAgICAgICAgICAgICAgICBjb2xfdHlwZXM9Y29scyhUaW1lPWNvbF9jaGFyYWN0ZXIoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ0N1bXVsYXRpdmUgVGltZSc9Y29sX2NoYXJhY3RlcigpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAnTW92aW5nIFRpbWUnPWNvbF9jaGFyYWN0ZXIoKQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICApDQogICAgICAgICAgICAgICAgICkNCg0KYGBgDQoNCjxkaXYgc3R5bGU9ImJhY2tncm91bmQtY29sb3I6I2VkZGRkNDsgbWFyZ2luOjEwcHgiPg0KPGRldGFpbHM+DQo8c3VtbWFyeT5FeHBsYW5hdGlvbiBvZiB0aGUgY29kZS48L3N1bW1hcnk+DQpUaGUgZnVuY3Rpb24gY2hvb3NlLmZpbGVzIHJldHVybnMgYSBsaXN0IG9mIGZpbGUgbmFtZXMgc2VsZWN0ZWQgYnkgdGhlIHVzZXIuICBUaGUgZmlsZSBuYW1lcyBhcmUgdXNlZCB0byByZWFkIGluIGFuZCBzdGl0Y2ggdG9nZXRoZXIgdGhlIGNob3NlZCBmaWxlcy4gIFRoZSBhc3N1bXB0aW9uIGlzIHRoYXQgdGhlIGZpbGVzIGFyZSBhbGwgb2YgdGhlIHNhbWUgc2hhcGUgYXMgZGVmaW5lZCBieSBHYXJtaW4gZm9yIHRoZSBzcGxpdHMgZXhwb3J0LiAgV2Ugb3B0aW1pc3RpY2FsbHkgYXNzdW1lIHRoZSBwYXRoIGlzIGFsd2F5cyBoYXBweS4NCiMjIyBUaGUgdmVjdG9yIG9mIGZ1bGwgc291cmNlIGZpbGUgbmFtZXMgY2hvc2VuIGJ5IHRoZSB1c2VyDQpgYGB7cn0NCiAgc291cmNlZmlsZXMNCmBgYA0KDQojIyMgVGhlIHZlY3RvcidzIGJhc2VuYW1lIHZhbHVlcyB0byBiZSB1c2VkIHRvIGNyZWF0ZSBhIG5ldyBjb2x1bW4gaW4gdGhlIGRhdGENCmBgYHtyfQ0KICBzb3VyY2VmaWxlcyAlPiUgYmFzZW5hbWUoKQ0KYGBgDQpOb3RlIHRoZSAlPiUuICBUaGlzIGlzIGEgcGlwZSBvcGVyYXRvciBmcm9tIHRoZSBtYWdyaXR0ciBwYWNrYWdlLCBwYXJ0IG9mIHRoZSB0aWR5dmVyc2UuICBUaGUgcGFja2FnZSBvZmZlcnMgYSBzZXQgb2Ygb3BlcmF0b3JzIHRoYXQgbWFrZSBjb2RlIG1vcmUgcmVhZGFibGUuICBJbiB0aGlzIGNhc2UsIHNvdXJjZWZpbGVzICh0aGUgdmVjdG9yIG9mIGZpbGUgbmFtZXMgY2hvc2VuIGJ5IHRoZSB1c2VyKSBpcyBwaXBlZCBhcyB0aGUgZmlyc3QgYXJndW1lbnQgaW50byB0aGUgZnVuY3Rpb24gYmFzZW5hbWUuICBUaGUgcmVzdWx0cyBvZiBiYXNlbmFtZSBhcmUgYXZhaWxhYmxlIHRvIGZlZWQgZm9yd2FyZCB0byBhbm90aGVyIGZ1bmN0aW9uLCBkZXBpY3RpbmcgYSBwaXBlbGluZSBvZiBwcm9jZXNzaW5nLg0KDQojIyMgVGhlIHZlY3RvcidzIGJhc2VuYW1lIHZhbHVlcyBhZGRlZCB0byBjcmVhdGUgYSBuYW1lZCB2ZWN0b3IuDQpgYGB7cn0NCiAgc291cmNlZmlsZXMgJT4lIHB1cnJyOjpzZXRfbmFtZXMobm09YmFzZW5hbWUoLikpDQpgYGANClRoZSBmdW5jdGlvbiBwdXJycjo6c2V0X25hbWVzIGFkZHMgbmFtZXMgdG8gdGhlIHZlY3RvciBmcm9tIHNvdXJjZWZpbGVzICh0aGUgc3RyaW5ncyByZXByZXNlbnRpbmcgdGhlIHNlbGVjdGVkIGZpbGVzKS4gIFRoZSBuYW1lcyBhZGRlZCB0byB0aGUgdmVjdG9yIGFyZSB0aG9zZSByZXR1cm5lZCBieSBiYXNlbmFtZSwgd2hpY2ggcmV1c2VzIHRoZSBhcmd1bWVudCBwaXBlZCB0byBzZXRfbmFtZXMgKGRlcGljdGVkIGJ5ICIuIikuICANCg0KIyMjIFRoZSBlbnRpcmUgcGlwZWxpbmUgdGhhdCBjcmVhdGVzIHRoZSB1bnRpZHkgZGF0YQ0KYGBge3IgZXZhbD1GQUxTRX0NCnVudGlkeSA8LSANCiAgc291cmNlZmlsZXMgJT4lIA0KICBwdXJycjo6c2V0X25hbWVzKG5tPWJhc2VuYW1lKC4pKSAlPiUNCiAgcHVycnI6Om1hcF9kZnIocmVhZHI6OnJlYWRfY3N2LCANCiAgICAgICAgICAgICAgICAgLmlkPSJzb3VyY2UiLCANCiAgICAgICAgICAgICAgICAgY29sX3R5cGVzPWNvbHMoVGltZT1jb2xfY2hhcmFjdGVyKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICdDdW11bGF0aXZlIFRpbWUnPWNvbF9jaGFyYWN0ZXIoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ01vdmluZyBUaW1lJz1jb2xfY2hhcmFjdGVyKCkNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgICAgICApDQpgYGANCg0KVGhlIGl0ZW1zIG9mIHRoZSBuYW1lZCB2ZWN0b3IgYXJlIG1hcHBlZCB0byB0aGUgcmVhZF9jc3YgZnVuY3Rpb24uICBBIG5ldyBjb2x1bW4gbmFtZWQgInNvdXJjZSIgaXMgYWRkZWQgdG8gdGhlIGRhdGEgZnJhbWUgYXMgdGhlIHByb2Nlc3MgZXhlY3V0ZXMuICBTaW5jZSB0aGUgaW5wdXQgaXMgYSBuYW1lZCB2ZWN0b3IsIHRoZSB2YWx1ZXMgZm9yIHRoZSBuZXcgY29sdW1uIGFyZSB0YWtlbiBmcm9tIHRoZSB2ZWN0b3IncyBuYW1lcy4gIFRoZSBmdWxsIGZpbGUgbmFtZSBpcyB1c2VkIHRvIGlkZW50aWZ5IHRoZSBmaWxlIHRvIHJlYWRfY3N2LiAgDQoNClRoZSBtYXBwaW5nIGZ1bmN0aW9uIGFkZHMgdGhlIGNvbF90eXBlcyBzdGFuemEgYXMgYW4gYXJndW1lbnQgdG8gdGhlIHJlYWRfY3N2IGZ1bmN0aW9uLiAgVGhlIHN0YW56YSB0ZWxscyByZWFkX2NzdiB0byBoYW5kbGUgc3BlY2lmaWMgQ1NWIGNvbHVtbnMgaW4gYSBzcGVjaWZpYyBtYW5uZXIuICBOYW1lZCBjb2x1bW5zIGFyZSBzZXQgdG8gYmUgdHJlYXRlZCBhcyBwbGFpbiB0ZXh0IHJhdGhlciB0aGFuIGluZmVycmluZyB0aGVpciB0eXBlIGZyb20gZGF0YSBhbmQgdHJhbnNmb3JtaW5nIHRoZSB2YWx1ZXMgYXMgdGhleSBhcmUgcmVhZC4gIA0KPC9kZXRhaWxzPg0KPC9kaXY+DQoNCiMjIyBVbnRpZHkgZGF0YQ0KYGBge3IgcmVzdWx0cz0nYXNpcyd9DQpEVDo6ZGF0YXRhYmxlKHVudGlkeSkNCmBgYA0KDQoNCiMjIFRpZHkgdGhlIGRhdGEgYW5kIGRlcml2ZSBuZXcgY29sdW1ucw0KDQpgYGB7cn0NCnRpZGllZCA8LSB1bnRpZHlbdW50aWR5JExhcHMgIT0gIlN1bW1hcnkiLCBdDQoNCnRpZGllZCRTZWNvbmRzIDwtIGx1YnJpZGF0ZTo6cGVyaW9kX3RvX3NlY29uZHMobXModGlkaWVkJFRpbWUpICkNCg0KdGlkaWVkJE1ldGVycyA8LSB0aWRpZWQkRGlzdGFuY2UgKiAxMDAwLjANCg0KdGlkaWVkJGRhc2gxMDBtc2VjcyA8LSByb3VuZCh0aWRpZWQkU2Vjb25kcyAvIHRpZGllZCRNZXRlcnMgKiAxMDAsIDEpDQoNCmludGVyZXN0aW5nIDwtIHRpZGllZCAlPiUgZHBseXI6OnNlbGVjdChzb3VyY2UsIExhcHMsIERpc3RhbmNlLCBUaW1lLCBNZXRlcnMsIFNlY29uZHMsIGRhc2gxMDBtc2VjcykNCmBgYA0KTmV3IGNvbHVtbnMgYXJlIGRlcml2ZWQgZnJvbSB0aGUgaW5wdXQgYW5kIGFkZGVkIHRvIHRoZSBkYXRhIGZyYW1lLiBUaGUgaW50ZXJlc3RpbmcgYml0cyBhcmUgcHVsbGVkIG91dC4NCg0KIyMgRGlzcGxheSB0aWR5IGRhdGENCg0KYGBge3IgcmVzdWx0cz0nYXNpcyd9DQptaW4oaW50ZXJlc3RpbmckZGFzaDEwMG1zZWNzKQ0KDQpEVDo6ZGF0YXRhYmxlKGludGVyZXN0aW5nKQ0KYGBgDQoNCiMjIFBsb3QgZGlzdHJpYnV0aW9uIG9mIHRpbWVzDQpgYGB7cn0NCnAgPC0gZ2dwbG90KHRpZGllZFt0aWRpZWQkZGFzaDEwMG1zZWNzIDwgMzAsIF0sIA0KICAgICAgICAgICAgYWVzKHNvdXJjZSwgZGFzaDEwMG1zZWNzLCBsYWJlbD1kYXNoMTAwbXNlY3MpKQ0KcCArIGdlb21fbGFiZWwobnVkZ2VfeCA9IDAuMikgKyANCiAgICBnZW9tX3BvaW50KGFlcyhjb2xvdXIgPSBmYWN0b3IoTWV0ZXJzKSkpDQpgYGANCg0K