This R Notebook is the complement to my blog post Playing with 80 Million Amazon Product Review Ratings Using Apache Spark.

This notebook is licensed under the MIT License. If you use the code or data visualization designs contained within this notebook, it would be greatly appreciated if proper attribution is given back to this notebook and/or myself. Thanks! :)

1 Setup

Setup the R packages.

library(dplyr)

Attaching package: ‘dplyr’

The following objects are masked from ‘package:stats’:

    filter, lag

The following objects are masked from ‘package:base’:

    intersect, setdiff, setequal, union
library(ggplot2)
library(sparklyr)
source("Rstart.R")
Registering fonts with R

Attaching package: ‘scales’

The following objects are masked from ‘package:readr’:

    col_factor, col_numeric
sessionInfo()
R version 3.3.2 (2016-10-31)
Platform: x86_64-apple-darwin13.4.0 (64-bit)
Running under: macOS Sierra 10.12.2

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

attached base packages:
[1] grid      stats     graphics  grDevices utils     datasets  methods  
[8] base     

other attached packages:
[1] stringr_1.1.0      digest_0.6.10      RColorBrewer_1.1-2
[4] scales_0.4.1       extrafont_0.17     readr_1.0.0       
[7] sparklyr_0.5.1     ggplot2_2.2.0      dplyr_0.5.0       

loaded via a namespace (and not attached):
 [1] Rcpp_0.12.8      Rttf2pt1_1.3.4   knitr_1.15.1     magrittr_1.5    
 [5] munsell_0.4.3    colorspace_1.3-2 R6_2.2.0         httr_1.2.1      
 [9] plyr_1.8.4       tools_3.3.2      gtable_0.2.0     DBI_0.5-1       
[13] extrafontdb_1.0  lazyeval_0.2.0   assertthat_0.1   rprojroot_1.1   
[17] tibble_1.2       base64enc_0.1-3  stringi_1.1.2    backports_1.0.4 
[21] jsonlite_1.1    

Note: I added sparklyr.shell.driver-memory: 8G to my Spark configuration profile to ensure there is enough memory for the entire dataset to fit in it for a single-client user. (otherwise it will write a LOT of data to disk!)

Usable memory is half the drver memory (since data is replicated 1x), so that config setting will fit 4 GB of data. (in theory; it will still write to disk often even if there is free memory available due to Spark’s architecture)

I use a 128GB Flash drive for the spark.local.dir as a scratch for Shuffle Write when necessary. (note; has 60MB/s write, so it will bottleneck on insane writes). However, it will not destroy valuable SSDs. Comment out the line if that is not an issue.

config <- spark_config()
config$`sparklyr.shell.driver-memory` <- "8G"
config$spark.local.dir <- "/Volumes/MAX128GB/tmp"
# Spark 2.1.0 was released but not supported by sparklyr at time of writing
sc <- spark_connect(master = "local", config = config, version="2.0.2")
sc$config
$sparklyr.cores.local
[1] 8

$spark.sql.shuffle.partitions.local
[1] 8

$spark.env.SPARK_LOCAL_IP.local
[1] "127.0.0.1"

$sparklyr.csv.embedded
[1] "^1.*"

$`sparklyr.shell.driver-class-path`
[1] ""

$`sparklyr.shell.driver-memory`
[1] "8G"

$spark.local.dir
[1] "/Volumes/MAX128GB/tmp"

attr(,"config")
[1] "default"
attr(,"file")
[1] "/Library/Frameworks/R.framework/Versions/3.3/Resources/library/sparklyr/conf/config-template.yml"
file_path <- "/Users/maxwoolf/Downloads/amazon_ratings"
# do not cache the initial data set to save memory; only cache the final result
df <- spark_read_csv(sc, 'data', paste(file_path, "amazon_ratings.csv", sep="/"), header=T, overwrite=T, memory=F)
df
Source:   query [8.074e+07 x 5]
Database: spark connection master=local[8] app=sparklyr local=TRUE

          user_id    item_id rating  timestamp             category
            <chr>      <chr>  <dbl>      <int>                <chr>
1  A1EE2E3N7PW666 B000GFDAUG      5 1202256000 Amazon Instant Video
2   AGZ8SM1BGK3CK B000GFDAUG      5 1198195200 Amazon Instant Video
3  A2VHZ21245KBT7 B000GIOPK2      4 1215388800 Amazon Instant Video
4   ACX8YW2D5EGP6 B000GIOPK2      4 1185840000 Amazon Instant Video
5   A9RNMO9MUSMTJ B000GIOPK2      2 1281052800 Amazon Instant Video
6  A3STFVPM8NHJ7B B000GIOPK2      5 1203897600 Amazon Instant Video
7  A2582KMXLK2P06 B000GIOPK2      5 1205884800 Amazon Instant Video
8  A1TZCLCW9QGGBH B000GIOPK2      4 1209427200 Amazon Instant Video
9  A2E2I6B878CRMA B000GIOPK2      5 1378684800 Amazon Instant Video
10  AD5MZA8SOVMPJ B000GIOPK2      5 1218240000 Amazon Instant Video
# ... with 8.074e+07 more rows

Dedupe to remove cases where reviews have the same user_id and item_id.

df <- df %>% group_by(user_id, item_id) %>% filter(row_number(item_id) == 1) %>% ungroup()

1.1 Additional Attributes

Add columns representing the timestamp as appropriate date/time values.

To convert an timestamp to a represting the timestamp, use from_unixtime (via Stack Overflow). Day of week requires a special function.

The time zone is in the time zone of the system running it (in my case, PST [GMT-7]). To compensate, I add an offset of 7+1 hours (the +1 due to Daylight Savings Time).

hours_offset <- 8
df <- df %>% mutate(timestamp_f = from_unixtime(timestamp + hours_offset*60*60))
df <- df %>% mutate(hour = hour(timestamp_f),
                    dayofweek = date_format(timestamp_f, 'EEEE'),
                    month = month(timestamp_f),
                    year = year(timestamp_f))

Append columns corresponding to the fact that a given rating is the user’s nth rating, and the item’s nth rating.

Because the timestamps are truncated to Day, ties are possible. If a tie is present, give the ranking the same nth value, since it’s better than omitting data.

df <- df %>% group_by(user_id) %>% mutate(user_nth = min_rank(timestamp)) %>% ungroup()
df <- df %>% group_by(item_id) %>% mutate(item_nth = min_rank(timestamp)) %>% ungroup()

Register the transformations as a new Spark DataTable and cache the result (since caches are immutable)

df_t <- df %>% sdf_register("data_t")
tbl_cache(sc, "data_t")
df_t
Source:   query [7.631e+07 x 12]
Database: spark connection master=local[8] app=sparklyr local=TRUE

          user_id    item_id rating  timestamp                   category
            <chr>      <chr>  <dbl>      <int>                      <chr>
1  A1TADCM7YWPQ8M 0000000868      4 1031702400                      Books
2  A1M7MWBQJEJWLY 0000031887      5 1260662400 Clothing Shoes and Jewelry
3  A3Q6CTO56DJ8UZ 0000031887      4 1268697600 Clothing Shoes and Jewelry
4  A338UR0Q3EG733 0000031887      5 1278979200 Clothing Shoes and Jewelry
5  A3H1W0DWLW5DLH 0000031887      5 1286668800 Clothing Shoes and Jewelry
6  A212HMSQUF21Z5 0000031887      5 1293408000 Clothing Shoes and Jewelry
7   AQ2NZ5X891VX1 0000031887      5 1295308800 Clothing Shoes and Jewelry
8  A2NXWW4QCL1EU1 0000031887      5 1296604800 Clothing Shoes and Jewelry
9  A12OFS8WQP86O5 0000031887      5 1297123200 Clothing Shoes and Jewelry
10 A1KLRMWW2FWPL4 0000031887      5 1297468800 Clothing Shoes and Jewelry
# ... with 7.631e+07 more rows, and 7 more variables: timestamp_f <chr>,
#   hour <int>, dayofweek <chr>, month <int>, year <int>, user_nth <int>,
#   item_nth <int>

Verify latter query worked correctly, and table cached correctly, by filtering on specific values. (cache tested by seeing if sql_render queries explicitly from data_t instead of a temp table)

sql_render(df_t %>% filter(user_id == "A3BUDYITWUSIS7"))
<SQL> SELECT *
FROM `data_t`
WHERE (`user_id` = 'A3BUDYITWUSIS7')
df_t %>% filter(user_id == "A3BUDYITWUSIS7") %>% collect()
df_t %>% filter(item_id == "0739048287") %>% collect()

2 Exploratory Analysis

2.1 Authors

Number of Unique authors:

df_user_review_counts <- df_t %>%
                          group_by(user_id) %>%
                          summarize(num_reviews=n()) %>%
                          group_by(num_reviews) %>%
                          summarize(total=n()) %>%
                          arrange(num_reviews) %>%
                          collect()
df_user_review_counts %>% head(100)

There are 20,368,412 unique users in this dataset. 51.9% have only left one review.

df_temp <- df_user_review_counts %>%
              mutate(norm = total/sum(total), prop = cumsum(norm)) %>%
              filter(num_reviews <= 50)
plot <- ggplot(df_temp, aes(x=num_reviews, y=prop)) +
          geom_line(color="#2980b9") +
          fte_theme() +
          scale_y_continuous(labels = percent, limits=c(0,1)) +
          labs(title="Cumulative Proportion of # Amazon Reviews Given by User", x="# Reviews Given By User", y="Cumulative Proportion of All Amazon Reviewers")
max_save(plot, "user_count_cum", "Amazon")
df_item_review_counts <- df_t %>%
                          group_by(item_id) %>%
                          summarize(num_reviews=n()) %>%
                          group_by(num_reviews) %>%
                          summarize(total=n()) %>%
                          arrange(num_reviews) %>%
                          collect()
df_item_review_counts %>% head(100)

There are 8,210,439 unique items in this dataset. 43.3% have only left one review.

df_temp <- df_item_review_counts %>%
              mutate(norm = total/sum(total), prop = cumsum(norm)) %>%
              filter(num_reviews <= 50)
plot <- ggplot(df_temp, aes(x=num_reviews, y=prop)) +
          geom_line(color="#27ae60") +
          fte_theme() +
          scale_y_continuous(labels = percent, limits=c(0,1)) +
          labs(title="Cumulative Proportion of # Reviews Given For Product", x="# Reviews Given For Product", y="Cumulative Proportion of All Amazon Products")
max_save(plot, "item_count_cum", "Amazon")

Average item rating by type.

df_agg <- df_t %>%
            group_by(category) %>%
            summarize(count = n(), avg_rating = mean(rating)) %>%
            arrange(desc(avg_rating)) %>%
            collect()
df_agg
df_temp <- df_agg
df_temp$category <- factor(df_temp$category, levels=rev(df_temp$category))
plot <- ggplot(df_temp, aes(x=category, y=avg_rating)) +
          geom_bar(stat="identity", fill="#e67e22", alpha=0.9) +
          geom_text(aes(label=sprintf("%0.2f", avg_rating)), color="white", hjust=1.25, family="Open Sans Condensed Bold") +
          fte_theme() +
          coord_flip() +
          scale_y_continuous(limits = c(1,5), oob = rescale_none) +
   theme(plot.title=element_text(vjust=0, hjust=1), axis.title.y=element_blank()) +
          labs(title="Average Rating Score Given For Amazon Reviews, by Product Category", y="Avg. Rating For Reviews Given in Category")
max_save(plot, "avg_rating_desc", "Amazon", h=6, tall=T)

Average rating by nth user review.

df_agg <- df_t %>%
            filter(user_nth <= 50) %>%
            group_by(user_nth) %>%
            summarize(count=n(), avg_rating = mean(rating)) %>%
            arrange(user_nth) %>%
            collect()
df_agg

Average rating by nth item review.

df_agg <- df_t %>%
            filter(item_nth <= 50) %>%
            group_by(item_nth) %>%
            summarize(count=n(), avg_rating = mean(rating)) %>%
            arrange(item_nth) %>%
            collect()
df_agg

Aggregate by Day of Week and Hour. Unfortunately, this confirms that timestamps are truncated to Day, making the Hour useless, and Day of Week, at the least, has no impact on average rating.

df_agg <- df_t %>%
            group_by(dayofweek, hour) %>%
            summarize(count=n(), avg_rating = mean(rating)) %>%
            arrange(dayofweek, hour) %>%
            collect()
df_agg

2.2 Time

Aggregate by Month and Year .

df_agg <- df_t %>%
            group_by(year, month) %>%
            filter(year >= 2000) %>%
            summarize(count=n(), avg_rating = mean(rating)) %>%
            arrange(year, month) %>%
            collect()
df_agg

Aggregate by Day of year. Can subset the formatted timestamp since no date column. Remove 1999 and 2014 since partial years.

There appear to be data irregularities causing dates like January 9th to appear, and more aggresive filtering on years changes the order completely. Therefor, this cannot be trusted or used in the blog post.

df_agg <- df_t %>%
            filter(year >= 2000, year < 2014) %>%
            mutate(date = substr(timestamp_f, 6, 5)) %>%
            group_by(date) %>%
            summarize(count=n(), avg_rating = mean(rating)) %>%
            arrange(desc(count)) %>%
            collect() %>%
            mutate(norm = round(count/sum(count), 5))
df_agg

3 Distributions of Ratings

Histogram of average ratings by users with atleast 5 reviews (rounded to nearest tenth)

df_agg_users <- df_t %>%
            group_by(user_id) %>%
            summarize(count=n(), avg_rating = round(mean(rating), 1)) %>%
            filter(count >= 5) %>%
            group_by(avg_rating) %>%
            summarize(count_avg = n()) %>%
            arrange(avg_rating) %>%
            collect()
df_agg_users

Get average of averages, on the given filter.

agg_users_true <- df_t %>%
            group_by(user_id) %>%
            summarize(count=n(), avg_rating = mean(rating)) %>%
            filter(count >= 5) %>%
            ungroup() %>%
            summarize(avg = mean(avg_rating)) %>%
            collect()
agg_users_true
plot <- ggplot(df_agg_users, aes(x=avg_rating, y=count_avg)) +
          geom_bar(stat="identity", fill='#2980b9') +
          geom_vline(xintercept=agg_users_true %>% unlist(), linetype="dashed") +
          fte_theme() +
          scale_y_continuous(labels = comma) +
          labs(title="Distribution of Average Ratings by User, for Amazon Products", x="Average Rating for Amazon Products Given By User (5 Ratings Minimum)", y="Count of Users")
max_save(plot, "user_histogram", "Amazon")

Histogram of average ratings for products with atleast 5 reviews (rounded to nearest tenth)

df_agg_items <- df_t %>%
            group_by(item_id) %>%
            summarize(count=n(), avg_rating = round(mean(rating), 1)) %>%
            filter(count >= 5) %>%
            group_by(avg_rating) %>%
            summarize(count_avg = n()) %>%
            arrange(avg_rating) %>%
            collect()
df_agg_items

Get average of averages, on the given filter.

agg_items_true <- df_t %>%
            group_by(item_id) %>%
            summarize(count=n(), avg_rating = mean(rating)) %>%
            filter(count >= 5) %>%
            ungroup() %>%
            summarize(avg = mean(avg_rating)) %>%
            collect()
agg_items_true
plot <- ggplot(df_agg_items, aes(x=avg_rating, y=count_avg)) +
          geom_bar(stat="identity", fill='#27ae60') +
          geom_vline(xintercept=agg_items_true %>% unlist(), linetype="dashed") +
          fte_theme() +
          scale_y_continuous(labels = comma) +
          labs(title="Distribution of Overall Ratings on Amazon Products", x="Overall Amazon Product Ratings (5 Ratings Minimum)", y="Count of Products")
max_save(plot, "item_histogram", "Amazon")

Merge the 2 charts together in a density plot.

temp <- (df_agg_users %>% mutate(category="Users", norm=count_avg/sum(count_avg))) %>% union_all(df_agg_items %>% mutate(category="Products", norm=count_avg/sum(count_avg)))
plot <- ggplot(temp, aes(x=avg_rating, y=norm, fill=category)) +
          geom_density(stat="identity", alpha=0.75, size=0.25) +
          fte_theme() +
          theme(legend.title = element_blank(), legend.position="top", legend.direction="horizontal", legend.key.width=unit(0.5, "cm"), legend.key.height=unit(0.0, "cm"), plot.title=element_text(vjust=0, hjust=0.5), legend.margin=margin(0, 0, -0.2, 0, "cm"), axis.text.y = element_blank(), axis.title.y=element_blank()) +
          scale_fill_manual(values=c('#27ae60', '#2980b9')) +
          labs(title="Distributions of Overall Ratings for Users and Amazon Products", x="Overall Ratings (5 Ratings Minimum)")
max_save(plot, "user_item_histogram", "Amazon")

4 Rating Breakdown Across nth Rating

rank_colors = c(brewer.pal(9, "Reds")[c(7,6,5)],brewer.pal(9, "Greens")[c(6,7)])

Breakdown accross nth user rating.

df_breakdown_users <- df_t %>%
            filter(user_nth <= 50) %>%
            group_by(user_nth, rating) %>%
            summarize(count=n()) %>%
            arrange(user_nth, rating) %>%
            collect()
df_breakdown_users %>% head(10)
plot <- ggplot(df_breakdown_users, aes(x=user_nth, y=count, fill=as.factor(rating))) +
          geom_bar(stat="identity", position = "fill") +
          fte_theme() +
          scale_y_continuous(labels = percent) +
  theme(legend.title = element_blank(), legend.position="top", legend.direction="horizontal", legend.key.width=unit(0.5, "cm"), legend.key.height=unit(0.0, "cm"), plot.title=element_text(vjust=0, hjust=1), legend.margin=margin(0, 0, -0.2, 0, "cm")) +
  scale_fill_manual(values=rank_colors, labels = c("1 Star", "2 Stars", "3 Stars", "4 Stars", "5 Stars")) +
          labs(title=expression(paste("Breakdown of Amazon Ratings Given by Users, by ", italic("n"), "th Rating Given")), x=expression(paste(italic("n"), "th Rating Given by User")), y="Proportion")
max_save(plot, "user_nth_breakdown", "Amazon")

Breakdown accross nth item rating.

df_breakdown_items <- df_t %>%
            filter(item_nth <= 50) %>%
            group_by(item_nth, rating) %>%
            summarize(count=n()) %>%
            arrange(item_nth, rating) %>%
            collect()
df_breakdown_items %>% head(10)
plot <- ggplot(df_breakdown_items, aes(x=item_nth, y=count, fill=as.factor(rating))) +
          geom_bar(stat="identity", position = "fill") +
          fte_theme() +
          scale_y_continuous(labels = percent) +
  theme(legend.title = element_blank(), legend.position="top", legend.direction="horizontal", legend.key.width=unit(0.5, "cm"), legend.key.height=unit(0.0, "cm"), plot.title=element_text(vjust=0, hjust=0.75), legend.margin=margin(0, 0, -0.2, 0, "cm")) +
  scale_fill_manual(values=rank_colors, labels = c("1 Star", "2 Stars", "3 Stars", "4 Stars", "5 Stars")) +
          labs(title=expression(paste("Breakdown of Ratings for Amazon Products by ", italic("n"), "th Item Rating Given")), x=expression(paste(italic("n"), "th Review for Item Given")), y="Proportion")
max_save(plot, "item_nth_breakdown", "Amazon")

Breakdown over time.

df_breakdown_time <- df_t %>%
            filter(year >= 2000) %>%
            group_by(year, month, rating) %>%
            summarize(count=n()) %>%
            arrange(year, month, rating) %>%
            collect() %>%
            mutate(date = as.Date(paste(year, month, "01", sep='-')))
df_breakdown_time %>% head(10)
plot <- ggplot(df_breakdown_time, aes(x=date, y=count, fill=as.factor(rating))) +
          geom_bar(stat="identity", position = "fill") +
          fte_theme() +
          scale_x_date(breaks=date_breaks('2 years'), date_labels = "%Y") +
          scale_y_continuous(labels = percent) +
  theme(legend.title = element_blank(), legend.position="top", legend.direction="horizontal", legend.key.width=unit(0.5, "cm"), legend.key.height=unit(0.0, "cm"), plot.title=element_text(vjust=0, hjust=1), legend.margin=margin(0, 0, -0.2, 0, "cm")) +
  scale_fill_manual(values=rank_colors, labels = c("1 Star", "2 Stars", "3 Stars", "4 Stars", "5 Stars")) +
          labs(title="Ratings Breakdown for Reviews Given on Amazon Products by Month", x="Month", y="Proportion")
max_save(plot, "time_breakdown", "Amazon")

Breakdown by Category (tricker to plot). Also include averages.

df_breakdown_category <- df_t %>%
            group_by(category, rating) %>%
            summarize(count=n()) %>%
            arrange(category, rating) %>%
            collect()
df_breakdown_category %>% head(10)
df_breakdown_category_avg  <- df_t %>%
            group_by(category) %>%
            summarize(avg_rating = mean(rating), sd_mean = sd(rating)/sqrt(n())) %>%
            arrange(desc(avg_rating)) %>%
            collect()
df_breakdown_category_avg
df_breakdown_category$category <- factor(df_breakdown_category$category, levels=rev(df_breakdown_category_avg$category))
plot <- ggplot(df_breakdown_category, aes(x=category, y=count, fill=as.factor(rating))) +
          geom_bar(stat="identity", position = "fill") +
          fte_theme() +
          coord_flip() +
          scale_y_continuous(labels = percent) +
  theme(legend.title = element_blank(), legend.position="top", legend.direction="horizontal", legend.key.width=unit(0.5, "cm"), legend.key.height=unit(0.0, "cm"), plot.title=element_text(vjust=0, hjust=1.5), legend.margin=margin(0, 0, -0.2, -3, "cm"), axis.title.y=element_blank()) +
  scale_fill_manual(values=rank_colors, labels = c("1 Star", "2 Stars", "3 Stars", "4 Stars", "5 Stars")) +
          labs(title="Breakdown of Amazon Ratings Given, by Product Category", y="Proportion")
max_save(plot, "category_breakdown", "Amazon", h=6, tall=T)

5 Code Which Did Not Work Out

5.1 RAMdisk

Create a 4GB RAMdisk (on OSX) for fastest writes during shuffles / spare the life of the SSDs.

Removed because Spark wants much more than 4GB! (final dataset required 12GB scratch)

ram_size_GB <- 4

system("diskutil unmount /Volumes/RAMdisk")
system(sprintf("diskutil erasevolume HFS+ 'RAMdisk' `hdiutil attach -nomount ram://%s`", ram_size_GB * 1024 * 2048))

5.2 Alternate Method of Reading Files into Spark

This method loads files into Spark without the Python preprocessing script, but due to how Spark works (has to repartition everytime a dataset is), it results in a lot of unnecessary disk activity (~50GB written!) since a shuffle write occurs for each concatenated dataset.

file_path <- "/Users/maxwoolf/Downloads/amazon_ratings"

files <- list.files(file_path)
files
extract_name <- function(x) {gsub("_", " ", substring(x, 9, nchar(x)-4))}
extract_name("ratings_Amazon_Instant_Video.csv")

Combine the dataframes together. Additionally, add a column representing the category of data the rating is from.

  • Read the data for a base table
  • To add the categories correctly, the evaluation must be nonlazy: use sdf_mutate() to force evaluation.
  • Read another table into a temp table, concatenate it to the normal data table, repeat.
clean_name <- extract_name(files[1])
df <- spark_read_csv(sc, 'load', paste(file_path, files[1], sep="/"), header=F, overwrite=T) %>%
    mutate(category = clean_name)%>%
    sdf_mutate()

for (file in files[-1]) {
  clean_name <- extract_name(file)
  temp <- spark_read_csv(sc, 'temp', paste(file_path, file, sep="/"), header=F, overwrite=T) %>%
    mutate(category = clean_name) %>%
    sdf_mutate()
  
  temp
  
  df <- df %>% union(temp)
  
  df
}


df <- df %>%
  rename(user_id = V1, item_id = V2, rating = V3, timestamp = V4) %>%
  sdf_register("data")

# cache table *before* calling it so latter call is free.
tbl_cache(sc, "data")
df 

# remove tables which no longer need to be used
tbl_uncache(sc, "load")
tbl_uncache(sc, "temp")

5.3 Dedupe (on Timestamp)

This was a dedupe to remove cases where reviews by a person had the same timestamp; the true cause of that behavior was the Day truncation of the timestamp.

df <- df %>% group_by(user_id, timestamp) %>% filter(row_number(timestamp) == 1) %>% ungroup()

6 LICENSE

The MIT License (MIT)

Copyright (c) 2017 Max Woolf

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

LS0tCnRpdGxlOiAiUGxheWluZyB3aXRoIDgwIE1pbGxpb24gQW1hem9uIFByb2R1Y3QgUmV2aWV3IFJhdGluZ3MgVXNpbmcgQXBhY2hlIFNwYXJrIgphdXRob3I6ICJNYXggV29vbGYgKEBtaW5pbWF4aXIpIgpkYXRlOiAiSmFudWFyeSAybmQsIDIwMTciCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgaGlnaGxpZ2h0OiB0YW5nbwogICAgbWF0aGpheDogbnVsbAogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMKICAgIHRoZW1lOiBzcGFjZWxhYgogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogeWVzCi0tLQoKVGhpcyBSIE5vdGVib29rIGlzIHRoZSBjb21wbGVtZW50IHRvIG15IGJsb2cgcG9zdCBbUGxheWluZyB3aXRoIDgwIE1pbGxpb24gQW1hem9uIFByb2R1Y3QgUmV2aWV3IFJhdGluZ3MgVXNpbmcgQXBhY2hlIFNwYXJrXShodHRwOi8vbWluaW1heGlyLmNvbS8yMDE3LzAxL2FtYXpvbi1zcGFyay8pLgoKVGhpcyBub3RlYm9vayBpcyBsaWNlbnNlZCB1bmRlciB0aGUgTUlUIExpY2Vuc2UuIElmIHlvdSB1c2UgdGhlIGNvZGUgb3IgZGF0YSB2aXN1YWxpemF0aW9uIGRlc2lnbnMgY29udGFpbmVkIHdpdGhpbiB0aGlzIG5vdGVib29rLCBpdCB3b3VsZCBiZSBncmVhdGx5IGFwcHJlY2lhdGVkIGlmIHByb3BlciBhdHRyaWJ1dGlvbiBpcyBnaXZlbiBiYWNrIHRvIHRoaXMgbm90ZWJvb2sgYW5kL29yIG15c2VsZi4gVGhhbmtzISA6KQoKIyBTZXR1cAoKU2V0dXAgdGhlIFIgcGFja2FnZXMuCgpgYGB7ciBzZXR1cH0KCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShzcGFya2x5cikKCnNvdXJjZSgiUnN0YXJ0LlIiKQpgYGAKCmBgYHtyfQpzZXNzaW9uSW5mbygpCmBgYAoKTm90ZTogSSBhZGRlZCBgc3BhcmtseXIuc2hlbGwuZHJpdmVyLW1lbW9yeTogOEdgIHRvIG15IFNwYXJrIGNvbmZpZ3VyYXRpb24gcHJvZmlsZSB0byBlbnN1cmUgdGhlcmUgaXMgZW5vdWdoIG1lbW9yeSBmb3IgdGhlIGVudGlyZSBkYXRhc2V0IHRvIGZpdCBpbiBpdCBmb3IgYSBzaW5nbGUtY2xpZW50IHVzZXIuIChvdGhlcndpc2UgaXQgd2lsbCB3cml0ZSBhIExPVCBvZiBkYXRhIHRvIGRpc2shKQoKVXNhYmxlIG1lbW9yeSBpcyBoYWxmIHRoZSBkcnZlciBtZW1vcnkgKHNpbmNlIGRhdGEgaXMgcmVwbGljYXRlZCAxeCksIHNvIHRoYXQgY29uZmlnIHNldHRpbmcgd2lsbCBmaXQgNCBHQiBvZiBkYXRhLiAoaW4gdGhlb3J5OyBpdCB3aWxsIHN0aWxsIHdyaXRlIHRvIGRpc2sgb2Z0ZW4gZXZlbiBpZiB0aGVyZSBpcyBmcmVlIG1lbW9yeSBhdmFpbGFibGUgZHVlIHRvIFNwYXJrJ3MgYXJjaGl0ZWN0dXJlKQoKSSB1c2UgYSAxMjhHQiBGbGFzaCBkcml2ZSBmb3IgdGhlIGBzcGFyay5sb2NhbC5kaXJgIGFzIGEgc2NyYXRjaCBmb3IgU2h1ZmZsZSBXcml0ZSB3aGVuIG5lY2Vzc2FyeS4gKG5vdGU7IGhhcyA2ME1CL3Mgd3JpdGUsIHNvIGl0IHdpbGwgYm90dGxlbmVjayBvbiBpbnNhbmUgd3JpdGVzKS4gSG93ZXZlciwgaXQgd2lsbCBub3QgZGVzdHJveSB2YWx1YWJsZSBTU0RzLiBDb21tZW50IG91dCB0aGUgbGluZSBpZiB0aGF0IGlzIG5vdCBhbiBpc3N1ZS4KCgpgYGB7cn0KY29uZmlnIDwtIHNwYXJrX2NvbmZpZygpCmNvbmZpZyRgc3BhcmtseXIuc2hlbGwuZHJpdmVyLW1lbW9yeWAgPC0gIjhHIgpjb25maWckc3BhcmsubG9jYWwuZGlyIDwtICIvVm9sdW1lcy9NQVgxMjhHQi90bXAiCgojIFNwYXJrIDIuMS4wIHdhcyByZWxlYXNlZCBidXQgbm90IHN1cHBvcnRlZCBieSBzcGFya2x5ciBhdCB0aW1lIG9mIHdyaXRpbmcKc2MgPC0gc3BhcmtfY29ubmVjdChtYXN0ZXIgPSAibG9jYWwiLCBjb25maWcgPSBjb25maWcsIHZlcnNpb249IjIuMC4yIikKc2MkY29uZmlnCmBgYAoKCmBgYHtyfQpmaWxlX3BhdGggPC0gIi9Vc2Vycy9tYXh3b29sZi9Eb3dubG9hZHMvYW1hem9uX3JhdGluZ3MiCgojIGRvIG5vdCBjYWNoZSB0aGUgaW5pdGlhbCBkYXRhIHNldCB0byBzYXZlIG1lbW9yeTsgb25seSBjYWNoZSB0aGUgZmluYWwgcmVzdWx0CgpkZiA8LSBzcGFya19yZWFkX2NzdihzYywgJ2RhdGEnLCBwYXN0ZShmaWxlX3BhdGgsICJhbWF6b25fcmF0aW5ncy5jc3YiLCBzZXA9Ii8iKSwgaGVhZGVyPVQsIG92ZXJ3cml0ZT1ULCBtZW1vcnk9RikKCmRmCmBgYAoKRGVkdXBlIHRvIHJlbW92ZSBjYXNlcyB3aGVyZSByZXZpZXdzIGhhdmUgdGhlIHNhbWUgYHVzZXJfaWRgIGFuZCBgaXRlbV9pZGAuCgpgYGB7cn0KZGYgPC0gZGYgJT4lIGdyb3VwX2J5KHVzZXJfaWQsIGl0ZW1faWQpICU+JSBmaWx0ZXIocm93X251bWJlcihpdGVtX2lkKSA9PSAxKSAlPiUgdW5ncm91cCgpCmBgYAoKIyMgQWRkaXRpb25hbCBBdHRyaWJ1dGVzCgpBZGQgY29sdW1ucyByZXByZXNlbnRpbmcgdGhlIHRpbWVzdGFtcCBhcyBhcHByb3ByaWF0ZSBkYXRlL3RpbWUgdmFsdWVzLgoKVG8gY29udmVydCBhbiA8aW50PiB0aW1lc3RhbXAgdG8gYSA8c3RyaW5nPiByZXByZXN0aW5nIHRoZSB0aW1lc3RhbXAsIHVzZSBgZnJvbV91bml4dGltZWAgKHZpYSBbU3RhY2sgT3ZlcmZsb3ddKGh0dHA6Ly9zdGFja292ZXJmbG93LmNvbS9hLzM3NTY5ODU1KSkuIERheSBvZiB3ZWVrIHJlcXVpcmVzIGEgW3NwZWNpYWwgZnVuY3Rpb25dKGh0dHA6Ly9zdGFja292ZXJmbG93LmNvbS9xdWVzdGlvbnMvMjUwMDY2MDcvaG93LXRvLWdldC1kYXktb2Ytd2Vlay1pbi1zcGFya3NxbCkuCgpUaGUgdGltZSB6b25lIGlzIGluIHRoZSB0aW1lIHpvbmUgb2YgdGhlIHN5c3RlbSBydW5uaW5nIGl0IChpbiBteSBjYXNlLCBQU1QgW0dNVC03XSkuIFRvIGNvbXBlbnNhdGUsIEkgYWRkIGFuIG9mZnNldCBvZiA3KzEgaG91cnMgKHRoZSArMSBkdWUgdG8gRGF5bGlnaHQgU2F2aW5ncyBUaW1lKS4KCmBgYHtyfQpob3Vyc19vZmZzZXQgPC0gOAoKZGYgPC0gZGYgJT4lIG11dGF0ZSh0aW1lc3RhbXBfZiA9IGZyb21fdW5peHRpbWUodGltZXN0YW1wICsgaG91cnNfb2Zmc2V0KjYwKjYwKSkKCmRmIDwtIGRmICU+JSBtdXRhdGUoaG91ciA9IGhvdXIodGltZXN0YW1wX2YpLAogICAgICAgICAgICAgICAgICAgIGRheW9md2VlayA9IGRhdGVfZm9ybWF0KHRpbWVzdGFtcF9mLCAnRUVFRScpLAogICAgICAgICAgICAgICAgICAgIG1vbnRoID0gbW9udGgodGltZXN0YW1wX2YpLAogICAgICAgICAgICAgICAgICAgIHllYXIgPSB5ZWFyKHRpbWVzdGFtcF9mKSkKYGBgCgpBcHBlbmQgY29sdW1ucyBjb3JyZXNwb25kaW5nIHRvIHRoZSBmYWN0IHRoYXQgYSBnaXZlbiByYXRpbmcgaXMgdGhlIHVzZXIncyAqbip0aCByYXRpbmcsIGFuZCB0aGUgaXRlbSdzICpuKnRoIHJhdGluZy4KCkJlY2F1c2UgdGhlIHRpbWVzdGFtcHMgYXJlIHRydW5jYXRlZCB0byBEYXksIHRpZXMgYXJlIHBvc3NpYmxlLiBJZiBhIHRpZSBpcyBwcmVzZW50LCBnaXZlIHRoZSByYW5raW5nIHRoZSBzYW1lICpuKnRoIHZhbHVlLCBzaW5jZSBpdCdzIGJldHRlciB0aGFuIG9taXR0aW5nIGRhdGEuCgpgYGB7cn0KZGYgPC0gZGYgJT4lIGdyb3VwX2J5KHVzZXJfaWQpICU+JSBtdXRhdGUodXNlcl9udGggPSBtaW5fcmFuayh0aW1lc3RhbXApKSAlPiUgdW5ncm91cCgpCgpkZiA8LSBkZiAlPiUgZ3JvdXBfYnkoaXRlbV9pZCkgJT4lIG11dGF0ZShpdGVtX250aCA9IG1pbl9yYW5rKHRpbWVzdGFtcCkpICU+JSB1bmdyb3VwKCkKYGBgCgpSZWdpc3RlciB0aGUgdHJhbnNmb3JtYXRpb25zIGFzIGEgbmV3IFNwYXJrIERhdGFUYWJsZSBhbmQgY2FjaGUgdGhlIHJlc3VsdCAoc2luY2UgY2FjaGVzIGFyZSBpbW11dGFibGUpCgpgYGB7cn0KZGZfdCA8LSBkZiAlPiUgc2RmX3JlZ2lzdGVyKCJkYXRhX3QiKQp0YmxfY2FjaGUoc2MsICJkYXRhX3QiKQoKZGZfdApgYGAKClZlcmlmeSBsYXR0ZXIgcXVlcnkgd29ya2VkIGNvcnJlY3RseSwgYW5kIHRhYmxlIGNhY2hlZCBjb3JyZWN0bHksIGJ5IGZpbHRlcmluZyBvbiBzcGVjaWZpYyB2YWx1ZXMuIChjYWNoZSB0ZXN0ZWQgYnkgc2VlaW5nIGlmIGBzcWxfcmVuZGVyYCBxdWVyaWVzIGV4cGxpY2l0bHkgZnJvbSBgZGF0YV90YCBpbnN0ZWFkIG9mIGEgdGVtcCB0YWJsZSkKCmBgYHtyfQpzcWxfcmVuZGVyKGRmX3QgJT4lIGZpbHRlcih1c2VyX2lkID09ICJBM0JVRFlJVFdVU0lTNyIpKQpkZl90ICU+JSBmaWx0ZXIodXNlcl9pZCA9PSAiQTNCVURZSVRXVVNJUzciKSAlPiUgY29sbGVjdCgpCmRmX3QgJT4lIGZpbHRlcihpdGVtX2lkID09ICIwNzM5MDQ4Mjg3IikgJT4lIGNvbGxlY3QoKQpgYGAKCiMgRXhwbG9yYXRvcnkgQW5hbHlzaXMKCiMjIEF1dGhvcnMKCk51bWJlciBvZiBVbmlxdWUgYXV0aG9yczoKCmBgYHtyfQpkZl91c2VyX3Jldmlld19jb3VudHMgPC0gZGZfdCAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICBncm91cF9ieSh1c2VyX2lkKSAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICBzdW1tYXJpemUobnVtX3Jldmlld3M9bigpKSAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICBncm91cF9ieShudW1fcmV2aWV3cykgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICAgc3VtbWFyaXplKHRvdGFsPW4oKSkgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICAgYXJyYW5nZShudW1fcmV2aWV3cykgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICAgY29sbGVjdCgpCgpkZl91c2VyX3Jldmlld19jb3VudHMgJT4lIGhlYWQoMTAwKQpgYGAKClRoZXJlIGFyZSAqKmByIGRmX3VzZXJfcmV2aWV3X2NvdW50cyAlPiUgc2VsZWN0KHRvdGFsKSAlPiUgc3VtKCkgJT4lIGZvcm1hdChiaWcubWFyaz0iLCIpYCoqIHVuaXF1ZSB1c2VycyBpbiB0aGlzIGRhdGFzZXQuICoqYHIgKGRmX3VzZXJfcmV2aWV3X2NvdW50cyR0b3RhbFsxXSAvIChkZl91c2VyX3Jldmlld19jb3VudHMgJT4lIHNlbGVjdCh0b3RhbCkgJT4lIHN1bSgpKSkgJT4lIHBlcmNlbnQoKWAqKiBoYXZlIG9ubHkgbGVmdCBvbmUgcmV2aWV3LgoKYGBge3J9CmRmX3RlbXAgPC0gZGZfdXNlcl9yZXZpZXdfY291bnRzICU+JQogICAgICAgICAgICAgIG11dGF0ZShub3JtID0gdG90YWwvc3VtKHRvdGFsKSwgcHJvcCA9IGN1bXN1bShub3JtKSkgJT4lCiAgICAgICAgICAgICAgZmlsdGVyKG51bV9yZXZpZXdzIDw9IDUwKQoKcGxvdCA8LSBnZ3Bsb3QoZGZfdGVtcCwgYWVzKHg9bnVtX3Jldmlld3MsIHk9cHJvcCkpICsKICAgICAgICAgIGdlb21fbGluZShjb2xvcj0iIzI5ODBiOSIpICsKICAgICAgICAgIGZ0ZV90aGVtZSgpICsKICAgICAgICAgIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBwZXJjZW50LCBsaW1pdHM9YygwLDEpKSArCiAgICAgICAgICBsYWJzKHRpdGxlPSJDdW11bGF0aXZlIFByb3BvcnRpb24gb2YgIyBBbWF6b24gUmV2aWV3cyBHaXZlbiBieSBVc2VyIiwgeD0iIyBSZXZpZXdzIEdpdmVuIEJ5IFVzZXIiLCB5PSJDdW11bGF0aXZlIFByb3BvcnRpb24gb2YgQWxsIEFtYXpvbiBSZXZpZXdlcnMiKQoKbWF4X3NhdmUocGxvdCwgInVzZXJfY291bnRfY3VtIiwgIkFtYXpvbiIpCmBgYAoKIVtdKHVzZXJfY291bnRfY3VtLnBuZykKCmBgYHtyfQpkZl9pdGVtX3Jldmlld19jb3VudHMgPC0gZGZfdCAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICBncm91cF9ieShpdGVtX2lkKSAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICBzdW1tYXJpemUobnVtX3Jldmlld3M9bigpKSAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICBncm91cF9ieShudW1fcmV2aWV3cykgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICAgc3VtbWFyaXplKHRvdGFsPW4oKSkgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICAgYXJyYW5nZShudW1fcmV2aWV3cykgJT4lCiAgICAgICAgICAgICAgICAgICAgICAgICAgY29sbGVjdCgpCgpkZl9pdGVtX3Jldmlld19jb3VudHMgJT4lIGhlYWQoMTAwKQpgYGAKClRoZXJlIGFyZSAqKmByIGRmX2l0ZW1fcmV2aWV3X2NvdW50cyAlPiUgc2VsZWN0KHRvdGFsKSAlPiUgc3VtKCkgJT4lIGZvcm1hdChiaWcubWFyaz0iLCIpYCoqIHVuaXF1ZSBpdGVtcyBpbiB0aGlzIGRhdGFzZXQuICAqKmByIChkZl9pdGVtX3Jldmlld19jb3VudHMkdG90YWxbMV0gLyAoZGZfaXRlbV9yZXZpZXdfY291bnRzICU+JSBzZWxlY3QodG90YWwpICU+JSBzdW0oKSkpICU+JSBwZXJjZW50KClgKiogaGF2ZSBvbmx5IGxlZnQgb25lIHJldmlldy4KCmBgYHtyfQpkZl90ZW1wIDwtIGRmX2l0ZW1fcmV2aWV3X2NvdW50cyAlPiUKICAgICAgICAgICAgICBtdXRhdGUobm9ybSA9IHRvdGFsL3N1bSh0b3RhbCksIHByb3AgPSBjdW1zdW0obm9ybSkpICU+JQogICAgICAgICAgICAgIGZpbHRlcihudW1fcmV2aWV3cyA8PSA1MCkKCnBsb3QgPC0gZ2dwbG90KGRmX3RlbXAsIGFlcyh4PW51bV9yZXZpZXdzLCB5PXByb3ApKSArCiAgICAgICAgICBnZW9tX2xpbmUoY29sb3I9IiMyN2FlNjAiKSArCiAgICAgICAgICBmdGVfdGhlbWUoKSArCiAgICAgICAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gcGVyY2VudCwgbGltaXRzPWMoMCwxKSkgKwogICAgICAgICAgbGFicyh0aXRsZT0iQ3VtdWxhdGl2ZSBQcm9wb3J0aW9uIG9mICMgUmV2aWV3cyBHaXZlbiBGb3IgUHJvZHVjdCIsIHg9IiMgUmV2aWV3cyBHaXZlbiBGb3IgUHJvZHVjdCIsIHk9IkN1bXVsYXRpdmUgUHJvcG9ydGlvbiBvZiBBbGwgQW1hem9uIFByb2R1Y3RzIikKCm1heF9zYXZlKHBsb3QsICJpdGVtX2NvdW50X2N1bSIsICJBbWF6b24iKQpgYGAKCiFbXShpdGVtX2NvdW50X2N1bS5wbmcpCgpBdmVyYWdlIGl0ZW0gcmF0aW5nIGJ5IHR5cGUuCgpgYGB7cn0KZGZfYWdnIDwtIGRmX3QgJT4lCiAgICAgICAgICAgIGdyb3VwX2J5KGNhdGVnb3J5KSAlPiUKICAgICAgICAgICAgc3VtbWFyaXplKGNvdW50ID0gbigpLCBhdmdfcmF0aW5nID0gbWVhbihyYXRpbmcpKSAlPiUKICAgICAgICAgICAgYXJyYW5nZShkZXNjKGF2Z19yYXRpbmcpKSAlPiUKICAgICAgICAgICAgY29sbGVjdCgpCgpkZl9hZ2cKYGBgCgpgYGB7cn0KZGZfdGVtcCA8LSBkZl9hZ2cKZGZfdGVtcCRjYXRlZ29yeSA8LSBmYWN0b3IoZGZfdGVtcCRjYXRlZ29yeSwgbGV2ZWxzPXJldihkZl90ZW1wJGNhdGVnb3J5KSkKCnBsb3QgPC0gZ2dwbG90KGRmX3RlbXAsIGFlcyh4PWNhdGVnb3J5LCB5PWF2Z19yYXRpbmcpKSArCiAgICAgICAgICBnZW9tX2JhcihzdGF0PSJpZGVudGl0eSIsIGZpbGw9IiNlNjdlMjIiLCBhbHBoYT0wLjkpICsKICAgICAgICAgIGdlb21fdGV4dChhZXMobGFiZWw9c3ByaW50ZigiJTAuMmYiLCBhdmdfcmF0aW5nKSksIGNvbG9yPSJ3aGl0ZSIsIGhqdXN0PTEuMjUsIGZhbWlseT0iT3BlbiBTYW5zIENvbmRlbnNlZCBCb2xkIikgKwogICAgICAgICAgZnRlX3RoZW1lKCkgKwogICAgICAgICAgY29vcmRfZmxpcCgpICsKICAgICAgICAgIHNjYWxlX3lfY29udGludW91cyhsaW1pdHMgPSBjKDEsNSksIG9vYiA9IHJlc2NhbGVfbm9uZSkgKwogICB0aGVtZShwbG90LnRpdGxlPWVsZW1lbnRfdGV4dCh2anVzdD0wLCBoanVzdD0xKSwgYXhpcy50aXRsZS55PWVsZW1lbnRfYmxhbmsoKSkgKwogICAgICAgICAgbGFicyh0aXRsZT0iQXZlcmFnZSBSYXRpbmcgU2NvcmUgR2l2ZW4gRm9yIEFtYXpvbiBSZXZpZXdzLCBieSBQcm9kdWN0IENhdGVnb3J5IiwgeT0iQXZnLiBSYXRpbmcgRm9yIFJldmlld3MgR2l2ZW4gaW4gQ2F0ZWdvcnkiKQoKbWF4X3NhdmUocGxvdCwgImF2Z19yYXRpbmdfZGVzYyIsICJBbWF6b24iLCBoPTYsIHRhbGw9VCkKYGBgCgohW10oYXZnX3JhdGluZ19kZXNjLnBuZykKCkF2ZXJhZ2UgcmF0aW5nIGJ5ICpuKnRoIHVzZXIgcmV2aWV3LgoKYGBge3J9CmRmX2FnZyA8LSBkZl90ICU+JQogICAgICAgICAgICBmaWx0ZXIodXNlcl9udGggPD0gNTApICU+JQogICAgICAgICAgICBncm91cF9ieSh1c2VyX250aCkgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShjb3VudD1uKCksIGF2Z19yYXRpbmcgPSBtZWFuKHJhdGluZykpICU+JQogICAgICAgICAgICBhcnJhbmdlKHVzZXJfbnRoKSAlPiUKICAgICAgICAgICAgY29sbGVjdCgpCgpkZl9hZ2cKYGBgCgpBdmVyYWdlIHJhdGluZyBieSAqbip0aCBpdGVtIHJldmlldy4KCmBgYHtyfQpkZl9hZ2cgPC0gZGZfdCAlPiUKICAgICAgICAgICAgZmlsdGVyKGl0ZW1fbnRoIDw9IDUwKSAlPiUKICAgICAgICAgICAgZ3JvdXBfYnkoaXRlbV9udGgpICU+JQogICAgICAgICAgICBzdW1tYXJpemUoY291bnQ9bigpLCBhdmdfcmF0aW5nID0gbWVhbihyYXRpbmcpKSAlPiUKICAgICAgICAgICAgYXJyYW5nZShpdGVtX250aCkgJT4lCiAgICAgICAgICAgIGNvbGxlY3QoKQoKZGZfYWdnCmBgYAoKQWdncmVnYXRlIGJ5IERheSBvZiBXZWVrIGFuZCBIb3VyLiBVbmZvcnR1bmF0ZWx5LCB0aGlzIGNvbmZpcm1zIHRoYXQgdGltZXN0YW1wcyBhcmUgdHJ1bmNhdGVkIHRvIERheSwgbWFraW5nIHRoZSBIb3VyIHVzZWxlc3MsIGFuZCBEYXkgb2YgV2VlaywgYXQgdGhlIGxlYXN0LCBoYXMgbm8gaW1wYWN0IG9uIGF2ZXJhZ2UgcmF0aW5nLgoKYGBge3J9CmRmX2FnZyA8LSBkZl90ICU+JQogICAgICAgICAgICBncm91cF9ieShkYXlvZndlZWssIGhvdXIpICU+JQogICAgICAgICAgICBzdW1tYXJpemUoY291bnQ9bigpLCBhdmdfcmF0aW5nID0gbWVhbihyYXRpbmcpKSAlPiUKICAgICAgICAgICAgYXJyYW5nZShkYXlvZndlZWssIGhvdXIpICU+JQogICAgICAgICAgICBjb2xsZWN0KCkKCmRmX2FnZwpgYGAKCiMjIFRpbWUKCkFnZ3JlZ2F0ZSBieSBNb250aCBhbmQgWWVhciAuCgpgYGB7cn0KZGZfYWdnIDwtIGRmX3QgJT4lCiAgICAgICAgICAgIGdyb3VwX2J5KHllYXIsIG1vbnRoKSAlPiUKICAgICAgICAgICAgZmlsdGVyKHllYXIgPj0gMjAwMCkgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShjb3VudD1uKCksIGF2Z19yYXRpbmcgPSBtZWFuKHJhdGluZykpICU+JQogICAgICAgICAgICBhcnJhbmdlKHllYXIsIG1vbnRoKSAlPiUKICAgICAgICAgICAgY29sbGVjdCgpCgpkZl9hZ2cKYGBgCgpBZ2dyZWdhdGUgYnkgRGF5IG9mIHllYXIuIENhbiBzdWJzZXQgdGhlIGZvcm1hdHRlZCB0aW1lc3RhbXAgc2luY2Ugbm8gZGF0ZSBjb2x1bW4uIFJlbW92ZSAxOTk5IGFuZCAyMDE0IHNpbmNlIHBhcnRpYWwgeWVhcnMuCgpUaGVyZSBhcHBlYXIgdG8gYmUgZGF0YSBpcnJlZ3VsYXJpdGllcyBjYXVzaW5nIGRhdGVzIGxpa2UgSmFudWFyeSA5dGggdG8gYXBwZWFyLCBhbmQgbW9yZSBhZ2dyZXNpdmUgZmlsdGVyaW5nIG9uIHllYXJzIGNoYW5nZXMgdGhlIG9yZGVyIGNvbXBsZXRlbHkuIFRoZXJlZm9yLCB0aGlzIGNhbm5vdCBiZSB0cnVzdGVkIG9yIHVzZWQgaW4gdGhlIGJsb2cgcG9zdC4KCmBgYHtyfQpkZl9hZ2cgPC0gZGZfdCAlPiUKICAgICAgICAgICAgZmlsdGVyKHllYXIgPj0gMjAwMCwgeWVhciA8IDIwMTQpICU+JQogICAgICAgICAgICBtdXRhdGUoZGF0ZSA9IHN1YnN0cih0aW1lc3RhbXBfZiwgNiwgNSkpICU+JQogICAgICAgICAgICBncm91cF9ieShkYXRlKSAlPiUKICAgICAgICAgICAgc3VtbWFyaXplKGNvdW50PW4oKSwgYXZnX3JhdGluZyA9IG1lYW4ocmF0aW5nKSkgJT4lCiAgICAgICAgICAgIGFycmFuZ2UoZGVzYyhjb3VudCkpICU+JQogICAgICAgICAgICBjb2xsZWN0KCkgJT4lCiAgICAgICAgICAgIG11dGF0ZShub3JtID0gcm91bmQoY291bnQvc3VtKGNvdW50KSwgNSkpCgpkZl9hZ2cKYGBgCgojIERpc3RyaWJ1dGlvbnMgb2YgUmF0aW5ncwoKSGlzdG9ncmFtIG9mIGF2ZXJhZ2UgcmF0aW5ncyBieSB1c2VycyB3aXRoIGF0bGVhc3QgNSByZXZpZXdzIChyb3VuZGVkIHRvIG5lYXJlc3QgdGVudGgpCgpgYGB7cn0KZGZfYWdnX3VzZXJzIDwtIGRmX3QgJT4lCiAgICAgICAgICAgIGdyb3VwX2J5KHVzZXJfaWQpICU+JQogICAgICAgICAgICBzdW1tYXJpemUoY291bnQ9bigpLCBhdmdfcmF0aW5nID0gcm91bmQobWVhbihyYXRpbmcpLCAxKSkgJT4lCiAgICAgICAgICAgIGZpbHRlcihjb3VudCA+PSA1KSAlPiUKICAgICAgICAgICAgZ3JvdXBfYnkoYXZnX3JhdGluZykgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShjb3VudF9hdmcgPSBuKCkpICU+JQogICAgICAgICAgICBhcnJhbmdlKGF2Z19yYXRpbmcpICU+JQogICAgICAgICAgICBjb2xsZWN0KCkKCmRmX2FnZ191c2VycwpgYGAKCkdldCBhdmVyYWdlIG9mIGF2ZXJhZ2VzLCBvbiB0aGUgZ2l2ZW4gZmlsdGVyLgoKYGBge3J9CmFnZ191c2Vyc190cnVlIDwtIGRmX3QgJT4lCiAgICAgICAgICAgIGdyb3VwX2J5KHVzZXJfaWQpICU+JQogICAgICAgICAgICBzdW1tYXJpemUoY291bnQ9bigpLCBhdmdfcmF0aW5nID0gbWVhbihyYXRpbmcpKSAlPiUKICAgICAgICAgICAgZmlsdGVyKGNvdW50ID49IDUpICU+JQogICAgICAgICAgICB1bmdyb3VwKCkgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShhdmcgPSBtZWFuKGF2Z19yYXRpbmcpKSAlPiUKICAgICAgICAgICAgY29sbGVjdCgpCgphZ2dfdXNlcnNfdHJ1ZQpgYGAKCgpgYGB7cn0KcGxvdCA8LSBnZ3Bsb3QoZGZfYWdnX3VzZXJzLCBhZXMoeD1hdmdfcmF0aW5nLCB5PWNvdW50X2F2ZykpICsKICAgICAgICAgIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5IiwgZmlsbD0nIzI5ODBiOScpICsKICAgICAgICAgIGdlb21fdmxpbmUoeGludGVyY2VwdD1hZ2dfdXNlcnNfdHJ1ZSAlPiUgdW5saXN0KCksIGxpbmV0eXBlPSJkYXNoZWQiKSArCiAgICAgICAgICBmdGVfdGhlbWUoKSArCiAgICAgICAgICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gY29tbWEpICsKICAgICAgICAgIGxhYnModGl0bGU9IkRpc3RyaWJ1dGlvbiBvZiBBdmVyYWdlIFJhdGluZ3MgYnkgVXNlciwgZm9yIEFtYXpvbiBQcm9kdWN0cyIsIHg9IkF2ZXJhZ2UgUmF0aW5nIGZvciBBbWF6b24gUHJvZHVjdHMgR2l2ZW4gQnkgVXNlciAoNSBSYXRpbmdzIE1pbmltdW0pIiwgeT0iQ291bnQgb2YgVXNlcnMiKQoKbWF4X3NhdmUocGxvdCwgInVzZXJfaGlzdG9ncmFtIiwgIkFtYXpvbiIpCmBgYAoKIVtdKHVzZXJfaGlzdG9ncmFtLnBuZykKCkhpc3RvZ3JhbSBvZiBhdmVyYWdlIHJhdGluZ3MgZm9yIHByb2R1Y3RzIHdpdGggYXRsZWFzdCA1IHJldmlld3MgKHJvdW5kZWQgdG8gbmVhcmVzdCB0ZW50aCkKCmBgYHtyfQpkZl9hZ2dfaXRlbXMgPC0gZGZfdCAlPiUKICAgICAgICAgICAgZ3JvdXBfYnkoaXRlbV9pZCkgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShjb3VudD1uKCksIGF2Z19yYXRpbmcgPSByb3VuZChtZWFuKHJhdGluZyksIDEpKSAlPiUKICAgICAgICAgICAgZmlsdGVyKGNvdW50ID49IDUpICU+JQogICAgICAgICAgICBncm91cF9ieShhdmdfcmF0aW5nKSAlPiUKICAgICAgICAgICAgc3VtbWFyaXplKGNvdW50X2F2ZyA9IG4oKSkgJT4lCiAgICAgICAgICAgIGFycmFuZ2UoYXZnX3JhdGluZykgJT4lCiAgICAgICAgICAgIGNvbGxlY3QoKQoKZGZfYWdnX2l0ZW1zCmBgYAoKR2V0IGF2ZXJhZ2Ugb2YgYXZlcmFnZXMsIG9uIHRoZSBnaXZlbiBmaWx0ZXIuCgpgYGB7cn0KYWdnX2l0ZW1zX3RydWUgPC0gZGZfdCAlPiUKICAgICAgICAgICAgZ3JvdXBfYnkoaXRlbV9pZCkgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShjb3VudD1uKCksIGF2Z19yYXRpbmcgPSBtZWFuKHJhdGluZykpICU+JQogICAgICAgICAgICBmaWx0ZXIoY291bnQgPj0gNSkgJT4lCiAgICAgICAgICAgIHVuZ3JvdXAoKSAlPiUKICAgICAgICAgICAgc3VtbWFyaXplKGF2ZyA9IG1lYW4oYXZnX3JhdGluZykpICU+JQogICAgICAgICAgICBjb2xsZWN0KCkKCmFnZ19pdGVtc190cnVlCmBgYAoKYGBge3J9CnBsb3QgPC0gZ2dwbG90KGRmX2FnZ19pdGVtcywgYWVzKHg9YXZnX3JhdGluZywgeT1jb3VudF9hdmcpKSArCiAgICAgICAgICBnZW9tX2JhcihzdGF0PSJpZGVudGl0eSIsIGZpbGw9JyMyN2FlNjAnKSArCiAgICAgICAgICBnZW9tX3ZsaW5lKHhpbnRlcmNlcHQ9YWdnX2l0ZW1zX3RydWUgJT4lIHVubGlzdCgpLCBsaW5ldHlwZT0iZGFzaGVkIikgKwogICAgICAgICAgZnRlX3RoZW1lKCkgKwogICAgICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IGNvbW1hKSArCiAgICAgICAgICBsYWJzKHRpdGxlPSJEaXN0cmlidXRpb24gb2YgT3ZlcmFsbCBSYXRpbmdzIG9uIEFtYXpvbiBQcm9kdWN0cyIsIHg9Ik92ZXJhbGwgQW1hem9uIFByb2R1Y3QgUmF0aW5nICg1IFJhdGluZ3MgTWluaW11bSkiLCB5PSJDb3VudCBvZiBQcm9kdWN0cyIpCgptYXhfc2F2ZShwbG90LCAiaXRlbV9oaXN0b2dyYW0iLCAiQW1hem9uIikKYGBgCgohW10oaXRlbV9oaXN0b2dyYW0ucG5nKQoKTWVyZ2UgdGhlIDIgY2hhcnRzIHRvZ2V0aGVyIGluIGEgZGVuc2l0eSBwbG90LgoKYGBge3J9CnRlbXAgPC0gKGRmX2FnZ191c2VycyAlPiUgbXV0YXRlKGNhdGVnb3J5PSJVc2VycyIsIG5vcm09Y291bnRfYXZnL3N1bShjb3VudF9hdmcpKSkgJT4lIHVuaW9uX2FsbChkZl9hZ2dfaXRlbXMgJT4lIG11dGF0ZShjYXRlZ29yeT0iUHJvZHVjdHMiLCBub3JtPWNvdW50X2F2Zy9zdW0oY291bnRfYXZnKSkpCgpwbG90IDwtIGdncGxvdCh0ZW1wLCBhZXMoeD1hdmdfcmF0aW5nLCB5PW5vcm0sIGZpbGw9Y2F0ZWdvcnkpKSArCiAgICAgICAgICBnZW9tX2RlbnNpdHkoc3RhdD0iaWRlbnRpdHkiLCBhbHBoYT0wLjc1LCBzaXplPTAuMjUpICsKICAgICAgICAgIGZ0ZV90aGVtZSgpICsKICAgICAgICAgIHRoZW1lKGxlZ2VuZC50aXRsZSA9IGVsZW1lbnRfYmxhbmsoKSwgbGVnZW5kLnBvc2l0aW9uPSJ0b3AiLCBsZWdlbmQuZGlyZWN0aW9uPSJob3Jpem9udGFsIiwgbGVnZW5kLmtleS53aWR0aD11bml0KDAuNSwgImNtIiksIGxlZ2VuZC5rZXkuaGVpZ2h0PXVuaXQoMC4wLCAiY20iKSwgcGxvdC50aXRsZT1lbGVtZW50X3RleHQodmp1c3Q9MCwgaGp1c3Q9MC41KSwgbGVnZW5kLm1hcmdpbj1tYXJnaW4oMCwgMCwgLTAuMiwgMCwgImNtIiksIGF4aXMudGV4dC55ID0gZWxlbWVudF9ibGFuaygpLCBheGlzLnRpdGxlLnk9ZWxlbWVudF9ibGFuaygpKSArCiAgICAgICAgICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygnIzI3YWU2MCcsICcjMjk4MGI5JykpICsKICAgICAgICAgIGxhYnModGl0bGU9IkRpc3RyaWJ1dGlvbnMgb2YgT3ZlcmFsbCBSYXRpbmdzIGZvciBVc2VycyBhbmQgQW1hem9uIFByb2R1Y3RzIiwgeD0iT3ZlcmFsbCBSYXRpbmdzICg1IFJhdGluZ3MgTWluaW11bSkiKQoKbWF4X3NhdmUocGxvdCwgInVzZXJfaXRlbV9oaXN0b2dyYW0iLCAiQW1hem9uIikKYGBgCgohW10odXNlcl9pdGVtX2hpc3RvZ3JhbS5wbmcpCgojIFJhdGluZyBCcmVha2Rvd24gQWNyb3NzICpuKnRoIFJhdGluZwoKYGBge3J9CnJhbmtfY29sb3JzID0gYyhicmV3ZXIucGFsKDksICJSZWRzIilbYyg3LDYsNSldLGJyZXdlci5wYWwoOSwgIkdyZWVucyIpW2MoNiw3KV0pCmBgYAoKCkJyZWFrZG93biBhY2Nyb3NzICpuKnRoIHVzZXIgcmF0aW5nLgoKYGBge3J9CmRmX2JyZWFrZG93bl91c2VycyA8LSBkZl90ICU+JQogICAgICAgICAgICBmaWx0ZXIodXNlcl9udGggPD0gNTApICU+JQogICAgICAgICAgICBncm91cF9ieSh1c2VyX250aCwgcmF0aW5nKSAlPiUKICAgICAgICAgICAgc3VtbWFyaXplKGNvdW50PW4oKSkgJT4lCiAgICAgICAgICAgIGFycmFuZ2UodXNlcl9udGgsIHJhdGluZykgJT4lCiAgICAgICAgICAgIGNvbGxlY3QoKQoKZGZfYnJlYWtkb3duX3VzZXJzICU+JSBoZWFkKDEwKQpgYGAKCmBgYHtyfQpwbG90IDwtIGdncGxvdChkZl9icmVha2Rvd25fdXNlcnMsIGFlcyh4PXVzZXJfbnRoLCB5PWNvdW50LCBmaWxsPWFzLmZhY3RvcihyYXRpbmcpKSkgKwogICAgICAgICAgZ2VvbV9iYXIoc3RhdD0iaWRlbnRpdHkiLCBwb3NpdGlvbiA9ICJmaWxsIikgKwogICAgICAgICAgZnRlX3RoZW1lKCkgKwogICAgICAgICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHBlcmNlbnQpICsKICB0aGVtZShsZWdlbmQudGl0bGUgPSBlbGVtZW50X2JsYW5rKCksIGxlZ2VuZC5wb3NpdGlvbj0idG9wIiwgbGVnZW5kLmRpcmVjdGlvbj0iaG9yaXpvbnRhbCIsIGxlZ2VuZC5rZXkud2lkdGg9dW5pdCgwLjUsICJjbSIpLCBsZWdlbmQua2V5LmhlaWdodD11bml0KDAuMCwgImNtIiksIHBsb3QudGl0bGU9ZWxlbWVudF90ZXh0KHZqdXN0PTAsIGhqdXN0PTEpLCBsZWdlbmQubWFyZ2luPW1hcmdpbigwLCAwLCAtMC4yLCAwLCAiY20iKSkgKwogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1yYW5rX2NvbG9ycywgbGFiZWxzID0gYygiMSBTdGFyIiwgIjIgU3RhcnMiLCAiMyBTdGFycyIsICI0IFN0YXJzIiwgIjUgU3RhcnMiKSkgKwogICAgICAgICAgbGFicyh0aXRsZT1leHByZXNzaW9uKHBhc3RlKCJCcmVha2Rvd24gb2YgQW1hem9uIFJhdGluZ3MgR2l2ZW4gYnkgVXNlcnMsIGJ5ICIsIGl0YWxpYygibiIpLCAidGggUmF0aW5nIEdpdmVuIikpLCB4PWV4cHJlc3Npb24ocGFzdGUoaXRhbGljKCJuIiksICJ0aCBSYXRpbmcgR2l2ZW4gYnkgVXNlciIpKSwgeT0iUHJvcG9ydGlvbiIpCgptYXhfc2F2ZShwbG90LCAidXNlcl9udGhfYnJlYWtkb3duIiwgIkFtYXpvbiIpCmBgYAoKIVtdKHVzZXJfbnRoX2JyZWFrZG93bi5wbmcpCgpCcmVha2Rvd24gYWNjcm9zcyAqbip0aCBpdGVtIHJhdGluZy4KCmBgYHtyfQpkZl9icmVha2Rvd25faXRlbXMgPC0gZGZfdCAlPiUKICAgICAgICAgICAgZmlsdGVyKGl0ZW1fbnRoIDw9IDUwKSAlPiUKICAgICAgICAgICAgZ3JvdXBfYnkoaXRlbV9udGgsIHJhdGluZykgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShjb3VudD1uKCkpICU+JQogICAgICAgICAgICBhcnJhbmdlKGl0ZW1fbnRoLCByYXRpbmcpICU+JQogICAgICAgICAgICBjb2xsZWN0KCkKCmRmX2JyZWFrZG93bl9pdGVtcyAlPiUgaGVhZCgxMCkKYGBgCgpgYGB7cn0KcGxvdCA8LSBnZ3Bsb3QoZGZfYnJlYWtkb3duX2l0ZW1zLCBhZXMoeD1pdGVtX250aCwgeT1jb3VudCwgZmlsbD1hcy5mYWN0b3IocmF0aW5nKSkpICsKICAgICAgICAgIGdlb21fYmFyKHN0YXQ9ImlkZW50aXR5IiwgcG9zaXRpb24gPSAiZmlsbCIpICsKICAgICAgICAgIGZ0ZV90aGVtZSgpICsKICAgICAgICAgIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBwZXJjZW50KSArCiAgdGhlbWUobGVnZW5kLnRpdGxlID0gZWxlbWVudF9ibGFuaygpLCBsZWdlbmQucG9zaXRpb249InRvcCIsIGxlZ2VuZC5kaXJlY3Rpb249Imhvcml6b250YWwiLCBsZWdlbmQua2V5LndpZHRoPXVuaXQoMC41LCAiY20iKSwgbGVnZW5kLmtleS5oZWlnaHQ9dW5pdCgwLjAsICJjbSIpLCBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dCh2anVzdD0wLCBoanVzdD0wLjc1KSwgbGVnZW5kLm1hcmdpbj1tYXJnaW4oMCwgMCwgLTAuMiwgMCwgImNtIikpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9cmFua19jb2xvcnMsIGxhYmVscyA9IGMoIjEgU3RhciIsICIyIFN0YXJzIiwgIjMgU3RhcnMiLCAiNCBTdGFycyIsICI1IFN0YXJzIikpICsKICAgICAgICAgIGxhYnModGl0bGU9ZXhwcmVzc2lvbihwYXN0ZSgiQnJlYWtkb3duIG9mIFJhdGluZ3MgZm9yIEFtYXpvbiBQcm9kdWN0cyBieSAiLCBpdGFsaWMoIm4iKSwgInRoIEl0ZW0gUmF0aW5nIEdpdmVuIikpLCB4PWV4cHJlc3Npb24ocGFzdGUoaXRhbGljKCJuIiksICJ0aCBSZXZpZXcgZm9yIEl0ZW0gR2l2ZW4iKSksIHk9IlByb3BvcnRpb24iKQoKbWF4X3NhdmUocGxvdCwgIml0ZW1fbnRoX2JyZWFrZG93biIsICJBbWF6b24iKQpgYGAKCiFbXShpdGVtX250aF9icmVha2Rvd24ucG5nKQoKQnJlYWtkb3duIG92ZXIgdGltZS4KCmBgYHtyfQpkZl9icmVha2Rvd25fdGltZSA8LSBkZl90ICU+JQogICAgICAgICAgICBmaWx0ZXIoeWVhciA+PSAyMDAwKSAlPiUKICAgICAgICAgICAgZ3JvdXBfYnkoeWVhciwgbW9udGgsIHJhdGluZykgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShjb3VudD1uKCkpICU+JQogICAgICAgICAgICBhcnJhbmdlKHllYXIsIG1vbnRoLCByYXRpbmcpICU+JQogICAgICAgICAgICBjb2xsZWN0KCkgJT4lCiAgICAgICAgICAgIG11dGF0ZShkYXRlID0gYXMuRGF0ZShwYXN0ZSh5ZWFyLCBtb250aCwgIjAxIiwgc2VwPSctJykpKQoKZGZfYnJlYWtkb3duX3RpbWUgJT4lIGhlYWQoMTApCmBgYAoKYGBge3J9CnBsb3QgPC0gZ2dwbG90KGRmX2JyZWFrZG93bl90aW1lLCBhZXMoeD1kYXRlLCB5PWNvdW50LCBmaWxsPWFzLmZhY3RvcihyYXRpbmcpKSkgKwogICAgICAgICAgZ2VvbV9iYXIoc3RhdD0iaWRlbnRpdHkiLCBwb3NpdGlvbiA9ICJmaWxsIikgKwogICAgICAgICAgZnRlX3RoZW1lKCkgKwogICAgICAgICAgc2NhbGVfeF9kYXRlKGJyZWFrcz1kYXRlX2JyZWFrcygnMiB5ZWFycycpLCBkYXRlX2xhYmVscyA9ICIlWSIpICsKICAgICAgICAgIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBwZXJjZW50KSArCiAgdGhlbWUobGVnZW5kLnRpdGxlID0gZWxlbWVudF9ibGFuaygpLCBsZWdlbmQucG9zaXRpb249InRvcCIsIGxlZ2VuZC5kaXJlY3Rpb249Imhvcml6b250YWwiLCBsZWdlbmQua2V5LndpZHRoPXVuaXQoMC41LCAiY20iKSwgbGVnZW5kLmtleS5oZWlnaHQ9dW5pdCgwLjAsICJjbSIpLCBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dCh2anVzdD0wLCBoanVzdD0xKSwgbGVnZW5kLm1hcmdpbj1tYXJnaW4oMCwgMCwgLTAuMiwgMCwgImNtIikpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9cmFua19jb2xvcnMsIGxhYmVscyA9IGMoIjEgU3RhciIsICIyIFN0YXJzIiwgIjMgU3RhcnMiLCAiNCBTdGFycyIsICI1IFN0YXJzIikpICsKICAgICAgICAgIGxhYnModGl0bGU9IlJhdGluZ3MgQnJlYWtkb3duIGZvciBSZXZpZXdzIEdpdmVuIG9uIEFtYXpvbiBQcm9kdWN0cyBieSBNb250aCIsIHg9Ik1vbnRoIiwgeT0iUHJvcG9ydGlvbiIpCgptYXhfc2F2ZShwbG90LCAidGltZV9icmVha2Rvd24iLCAiQW1hem9uIikKYGBgCgohW10odGltZV9icmVha2Rvd24ucG5nKQoKQnJlYWtkb3duIGJ5IENhdGVnb3J5ICh0cmlja2VyIHRvIHBsb3QpLiBBbHNvIGluY2x1ZGUgYXZlcmFnZXMuCgpgYGB7cn0KZGZfYnJlYWtkb3duX2NhdGVnb3J5IDwtIGRmX3QgJT4lCiAgICAgICAgICAgIGdyb3VwX2J5KGNhdGVnb3J5LCByYXRpbmcpICU+JQogICAgICAgICAgICBzdW1tYXJpemUoY291bnQ9bigpKSAlPiUKICAgICAgICAgICAgYXJyYW5nZShjYXRlZ29yeSwgcmF0aW5nKSAlPiUKICAgICAgICAgICAgY29sbGVjdCgpCgpkZl9icmVha2Rvd25fY2F0ZWdvcnkgJT4lIGhlYWQoMTApCgpkZl9icmVha2Rvd25fY2F0ZWdvcnlfYXZnICA8LSBkZl90ICU+JQogICAgICAgICAgICBncm91cF9ieShjYXRlZ29yeSkgJT4lCiAgICAgICAgICAgIHN1bW1hcml6ZShhdmdfcmF0aW5nID0gbWVhbihyYXRpbmcpLCBzZF9tZWFuID0gc2QocmF0aW5nKS9zcXJ0KG4oKSkpICU+JQogICAgICAgICAgICBhcnJhbmdlKGRlc2MoYXZnX3JhdGluZykpICU+JQogICAgICAgICAgICBjb2xsZWN0KCkKCmRmX2JyZWFrZG93bl9jYXRlZ29yeV9hdmcKYGBgCgpgYGB7cn0KZGZfYnJlYWtkb3duX2NhdGVnb3J5JGNhdGVnb3J5IDwtIGZhY3RvcihkZl9icmVha2Rvd25fY2F0ZWdvcnkkY2F0ZWdvcnksIGxldmVscz1yZXYoZGZfYnJlYWtkb3duX2NhdGVnb3J5X2F2ZyRjYXRlZ29yeSkpCgpwbG90IDwtIGdncGxvdChkZl9icmVha2Rvd25fY2F0ZWdvcnksIGFlcyh4PWNhdGVnb3J5LCB5PWNvdW50LCBmaWxsPWFzLmZhY3RvcihyYXRpbmcpKSkgKwogICAgICAgICAgZ2VvbV9iYXIoc3RhdD0iaWRlbnRpdHkiLCBwb3NpdGlvbiA9ICJmaWxsIikgKwogICAgICAgICAgZnRlX3RoZW1lKCkgKwogICAgICAgICAgY29vcmRfZmxpcCgpICsKICAgICAgICAgIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBwZXJjZW50KSArCiAgdGhlbWUobGVnZW5kLnRpdGxlID0gZWxlbWVudF9ibGFuaygpLCBsZWdlbmQucG9zaXRpb249InRvcCIsIGxlZ2VuZC5kaXJlY3Rpb249Imhvcml6b250YWwiLCBsZWdlbmQua2V5LndpZHRoPXVuaXQoMC41LCAiY20iKSwgbGVnZW5kLmtleS5oZWlnaHQ9dW5pdCgwLjAsICJjbSIpLCBwbG90LnRpdGxlPWVsZW1lbnRfdGV4dCh2anVzdD0wLCBoanVzdD0xLjUpLCBsZWdlbmQubWFyZ2luPW1hcmdpbigwLCAwLCAtMC4yLCAtMywgImNtIiksIGF4aXMudGl0bGUueT1lbGVtZW50X2JsYW5rKCkpICsKICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9cmFua19jb2xvcnMsIGxhYmVscyA9IGMoIjEgU3RhciIsICIyIFN0YXJzIiwgIjMgU3RhcnMiLCAiNCBTdGFycyIsICI1IFN0YXJzIikpICsKICAgICAgICAgIGxhYnModGl0bGU9IkJyZWFrZG93biBvZiBBbWF6b24gUmF0aW5ncyBHaXZlbiwgYnkgUHJvZHVjdCBDYXRlZ29yeSIsIHk9IlByb3BvcnRpb24iKQoKbWF4X3NhdmUocGxvdCwgImNhdGVnb3J5X2JyZWFrZG93biIsICJBbWF6b24iLCBoPTYsIHRhbGw9VCkKYGBgCgohW10oY2F0ZWdvcnlfYnJlYWtkb3duLnBuZykKCiMgQ29kZSBXaGljaCBEaWQgTm90IFdvcmsgT3V0CgojIyBSQU1kaXNrCgpDcmVhdGUgYSA0R0IgUkFNZGlzayAob24gT1NYKSBmb3IgZmFzdGVzdCB3cml0ZXMgZHVyaW5nIHNodWZmbGVzIC8gc3BhcmUgdGhlIGxpZmUgb2YgdGhlIFNTRHMuCgpSZW1vdmVkIGJlY2F1c2UgU3Bhcmsgd2FudHMgKm11Y2gqIG1vcmUgdGhhbiA0R0IhIChmaW5hbCBkYXRhc2V0IHJlcXVpcmVkIDEyR0Igc2NyYXRjaCkKCmBgYHtyLCBldmFsPUZBTFNFfQpyYW1fc2l6ZV9HQiA8LSA0CgpzeXN0ZW0oImRpc2t1dGlsIHVubW91bnQgL1ZvbHVtZXMvUkFNZGlzayIpCnN5c3RlbShzcHJpbnRmKCJkaXNrdXRpbCBlcmFzZXZvbHVtZSBIRlMrICdSQU1kaXNrJyBgaGRpdXRpbCBhdHRhY2ggLW5vbW91bnQgcmFtOi8vJXNgIiwgcmFtX3NpemVfR0IgKiAxMDI0ICogMjA0OCkpCmBgYAoKIyMgQWx0ZXJuYXRlIE1ldGhvZCBvZiBSZWFkaW5nIEZpbGVzIGludG8gU3BhcmsKClRoaXMgbWV0aG9kIGxvYWRzIGZpbGVzIGludG8gU3Bhcmsgd2l0aG91dCB0aGUgUHl0aG9uIHByZXByb2Nlc3Npbmcgc2NyaXB0LCBidXQgZHVlIHRvIGhvdyBTcGFyayB3b3JrcyAoaGFzIHRvIHJlcGFydGl0aW9uIGV2ZXJ5dGltZSBhIGRhdGFzZXQgaXMpLCBpdCByZXN1bHRzIGluIGEgbG90IG9mIHVubmVjZXNzYXJ5IGRpc2sgYWN0aXZpdHkgKH41MEdCIHdyaXR0ZW4hKSBzaW5jZSBhIHNodWZmbGUgd3JpdGUgb2NjdXJzIGZvciAqZWFjaCogY29uY2F0ZW5hdGVkIGRhdGFzZXQuCgpgYGB7ciwgZXZhbD1GQUxTRX0KZmlsZV9wYXRoIDwtICIvVXNlcnMvbWF4d29vbGYvRG93bmxvYWRzL2FtYXpvbl9yYXRpbmdzIgoKZmlsZXMgPC0gbGlzdC5maWxlcyhmaWxlX3BhdGgpCmZpbGVzCmBgYAoKYGBge3IsIGV2YWw9RkFMU0V9CmV4dHJhY3RfbmFtZSA8LSBmdW5jdGlvbih4KSB7Z3N1YigiXyIsICIgIiwgc3Vic3RyaW5nKHgsIDksIG5jaGFyKHgpLTQpKX0KZXh0cmFjdF9uYW1lKCJyYXRpbmdzX0FtYXpvbl9JbnN0YW50X1ZpZGVvLmNzdiIpCmBgYAoKQ29tYmluZSB0aGUgZGF0YWZyYW1lcyB0b2dldGhlci4gQWRkaXRpb25hbGx5LCBhZGQgYSBjb2x1bW4gcmVwcmVzZW50aW5nIHRoZSBjYXRlZ29yeSBvZiBkYXRhIHRoZSByYXRpbmcgaXMgZnJvbS4KCiogUmVhZCB0aGUgZGF0YSBmb3IgYSBiYXNlIHRhYmxlCiogVG8gYWRkIHRoZSBjYXRlZ29yaWVzIGNvcnJlY3RseSwgdGhlIGV2YWx1YXRpb24gbXVzdCBiZSBub25sYXp5OiB1c2UgYHNkZl9tdXRhdGUoKWAgdG8gZm9yY2UgZXZhbHVhdGlvbi4KKiBSZWFkIGFub3RoZXIgdGFibGUgaW50byBhIHRlbXAgdGFibGUsIGNvbmNhdGVuYXRlIGl0IHRvIHRoZSBub3JtYWwgZGF0YSB0YWJsZSwgcmVwZWF0LgoKYGBge3IsIGV2YWw9RkFMU0V9CmNsZWFuX25hbWUgPC0gZXh0cmFjdF9uYW1lKGZpbGVzWzFdKQpkZiA8LSBzcGFya19yZWFkX2NzdihzYywgJ2xvYWQnLCBwYXN0ZShmaWxlX3BhdGgsIGZpbGVzWzFdLCBzZXA9Ii8iKSwgaGVhZGVyPUYsIG92ZXJ3cml0ZT1UKSAlPiUKICAgIG11dGF0ZShjYXRlZ29yeSA9IGNsZWFuX25hbWUpJT4lCiAgICBzZGZfbXV0YXRlKCkKCmZvciAoZmlsZSBpbiBmaWxlc1stMV0pIHsKICBjbGVhbl9uYW1lIDwtIGV4dHJhY3RfbmFtZShmaWxlKQogIHRlbXAgPC0gc3BhcmtfcmVhZF9jc3Yoc2MsICd0ZW1wJywgcGFzdGUoZmlsZV9wYXRoLCBmaWxlLCBzZXA9Ii8iKSwgaGVhZGVyPUYsIG92ZXJ3cml0ZT1UKSAlPiUKICAgIG11dGF0ZShjYXRlZ29yeSA9IGNsZWFuX25hbWUpICU+JQogICAgc2RmX211dGF0ZSgpCiAgCiAgdGVtcAogIAogIGRmIDwtIGRmICU+JSB1bmlvbih0ZW1wKQogIAogIGRmCn0KCgpkZiA8LSBkZiAlPiUKICByZW5hbWUodXNlcl9pZCA9IFYxLCBpdGVtX2lkID0gVjIsIHJhdGluZyA9IFYzLCB0aW1lc3RhbXAgPSBWNCkgJT4lCiAgc2RmX3JlZ2lzdGVyKCJkYXRhIikKCiMgY2FjaGUgdGFibGUgKmJlZm9yZSogY2FsbGluZyBpdCBzbyBsYXR0ZXIgY2FsbCBpcyBmcmVlLgp0YmxfY2FjaGUoc2MsICJkYXRhIikKZGYgCgojIHJlbW92ZSB0YWJsZXMgd2hpY2ggbm8gbG9uZ2VyIG5lZWQgdG8gYmUgdXNlZAp0YmxfdW5jYWNoZShzYywgImxvYWQiKQp0YmxfdW5jYWNoZShzYywgInRlbXAiKQpgYGAKCiMjIERlZHVwZSAob24gVGltZXN0YW1wKQoKVGhpcyB3YXMgYSBkZWR1cGUgdG8gcmVtb3ZlIGNhc2VzIHdoZXJlIHJldmlld3MgYnkgYSBwZXJzb24gaGFkIHRoZSBzYW1lIHRpbWVzdGFtcDsgdGhlIHRydWUgY2F1c2Ugb2YgdGhhdCBiZWhhdmlvciB3YXMgdGhlIERheSB0cnVuY2F0aW9uIG9mIHRoZSB0aW1lc3RhbXAuCgpgYGB7ciwgZXZhbD1GQUxTRX0KZGYgPC0gZGYgJT4lIGdyb3VwX2J5KHVzZXJfaWQsIHRpbWVzdGFtcCkgJT4lIGZpbHRlcihyb3dfbnVtYmVyKHRpbWVzdGFtcCkgPT0gMSkgJT4lIHVuZ3JvdXAoKQpgYGAKCiMgTElDRU5TRQoKVGhlIE1JVCBMaWNlbnNlIChNSVQpCgpDb3B5cmlnaHQgKGMpIDIwMTcgTWF4IFdvb2xmCgpQZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYSBjb3B5IG9mIHRoaXMgc29mdHdhcmUgYW5kIGFzc29jaWF0ZWQgZG9jdW1lbnRhdGlvbiBmaWxlcyAodGhlICJTb2Z0d2FyZSIpLCB0byBkZWFsIGluIHRoZSBTb2Z0d2FyZSB3aXRob3V0IHJlc3RyaWN0aW9uLCBpbmNsdWRpbmcgd2l0aG91dCBsaW1pdGF0aW9uIHRoZSByaWdodHMgdG8gdXNlLCBjb3B5LCBtb2RpZnksIG1lcmdlLCBwdWJsaXNoLCBkaXN0cmlidXRlLCBzdWJsaWNlbnNlLCBhbmQvb3Igc2VsbCBjb3BpZXMgb2YgdGhlIFNvZnR3YXJlLCBhbmQgdG8gcGVybWl0IHBlcnNvbnMgdG8gd2hvbSB0aGUgU29mdHdhcmUgaXMgZnVybmlzaGVkIHRvIGRvIHNvLCBzdWJqZWN0IHRvIHRoZSBmb2xsb3dpbmcgY29uZGl0aW9uczoKClRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIHNoYWxsIGJlIGluY2x1ZGVkIGluIGFsbCBjb3BpZXMgb3Igc3Vic3RhbnRpYWwgcG9ydGlvbnMgb2YgdGhlIFNvZnR3YXJlLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIsIFdJVEhPVVQgV0FSUkFOVFkgT0YgQU5ZIEtJTkQsIEVYUFJFU1MgT1IgSU1QTElFRCwgSU5DTFVESU5HIEJVVCBOT1QgTElNSVRFRCBUTyBUSEUgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFksIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFIEFORCBOT05JTkZSSU5HRU1FTlQuIElOIE5PIEVWRU5UIFNIQUxMIFRIRSBBVVRIT1JTIE9SIENPUFlSSUdIVCBIT0xERVJTIEJFIExJQUJMRSBGT1IgQU5ZIENMQUlNLCBEQU1BR0VTIE9SIE9USEVSIExJQUJJTElUWSwgV0hFVEhFUiBJTiBBTiBBQ1RJT04gT0YgQ09OVFJBQ1QsIFRPUlQgT1IgT1RIRVJXSVNFLCBBUklTSU5HIEZST00sIE9VVCBPRiBPUiBJTiBDT05ORUNUSU9OIFdJVEggVEhFIFNPRlRXQVJFIE9SIFRIRSBVU0UgT1IgT1RIRVIgREVBTElOR1MgSU4gVEhFIFNPRlRXQVJFLg==