Last updated: 2021-10-11

Checks: 7 0

Knit directory: myTidyTuesday/

This reproducible R Markdown analysis was created with workflowr (version 1.6.2). The Checks tab describes the reproducibility checks that were applied when the results were created. The Past versions tab lists the development history.


Great! Since the R Markdown file has been committed to the Git repository, you know the exact version of the code that produced these results.

Great job! The global environment was empty. Objects defined in the global environment can affect the analysis in your R Markdown file in unknown ways. For reproduciblity it’s best to always run the code in an empty environment.

The command set.seed(20210907) was run prior to running the code in the R Markdown file. Setting a seed ensures that any results that rely on randomness, e.g. subsampling or permutations, are reproducible.

Great job! Recording the operating system, R version, and package versions is critical for reproducibility.

Nice! There were no cached chunks for this analysis, so you can be confident that you successfully produced the results during this run.

Great job! Using relative paths to the files within your workflowr project makes it easier to run your code on other machines.

Great! You are using Git for version control. Tracking code development and connecting the code version to the results is critical for reproducibility.

The results in this page were generated with repository version 1be4dee. See the Past versions tab to see a history of the changes made to the R Markdown and HTML files.

Note that you need to be careful to ensure that all relevant files for the analysis have been committed to Git prior to generating the results (you can use wflow_publish or wflow_git_commit). workflowr only checks the R Markdown file, but you know if there are other scripts or data files that it depends on. Below is the status of the Git repository when the results were generated:


Ignored files:
    Ignored:    .Rhistory
    Ignored:    .Rproj.user/
    Ignored:    catboost_info/
    Ignored:    data/2021-10-11/
    Ignored:    data/CNHI_Excel_Chart.xlsx
    Ignored:    data/CommunityTreemap.jpeg
    Ignored:    data/Community_Roles.jpeg
    Ignored:    data/YammerDigitalDataScienceMembership.xlsx
    Ignored:    data/accountchurn.rds
    Ignored:    data/acs_poverty.rds
    Ignored:    data/advancedaccountchurn.rds
    Ignored:    data/airbnbcatboost.rds
    Ignored:    data/australiaweather.rds
    Ignored:    data/fmhpi.rds
    Ignored:    data/grainstocks.rds
    Ignored:    data/hike_data.rds
    Ignored:    data/nber_rs.rmd
    Ignored:    data/netflixTitles.rmd
    Ignored:    data/netflixTitles2.rds
    Ignored:    data/spotifyxgboost.rds
    Ignored:    data/spotifyxgboostadvanced.rds
    Ignored:    data/us_states.rds
    Ignored:    data/us_states_hexgrid.geojson
    Ignored:    data/weatherstats_toronto_daily.csv

Untracked files:
    Untracked:  analysis/CHN_1_sp.rds
    Untracked:  analysis/sample data for r test.xlsx
    Untracked:  code/YammerReach.R
    Untracked:  code/work list batch targets.R

Note that any generated files, e.g. HTML, png, CSS, etc., are not included in this status report because it is ok for generated content to have uncommitted changes.


These are the previous versions of the repository in which changes were made to the R Markdown (analysis/2021_07_20_sliced.Rmd) and HTML (docs/2021_07_20_sliced.html) files. If you’ve configured a remote Git repository (see ?wflow_git_remote), click on the hyperlinks in the table below to view the files as they were in that past version.

File Version Author Date Message
Rmd 1be4dee opus1993 2021-10-11 adopt common color palette

Season 1 Episode 8 features a challenge to predict the popularity of Spotify tracks. The evaluation metric for submissions in this competition is residual mean squared error.

SLICED is like the TV Show Chopped but for data science. Competitors get a never-before-seen dataset and two-hours to code a solution to a prediction challenge. Contestants get points for the best model plus bonus points for data visualization, votes from the audience, and more.

The audience is invited to participate as well. This file consists of my submissions with cleanup and commentary added.

To make the best use of the resources that we have, we will explore the data set for features to select those with the most predictive power, build a random forest to confirm the recipe, and then build one or more ensemble models. If there is time, we will craft some visuals for model explainability.

Let’s load up packages:

suppressPackageStartupMessages({
library(tidyverse) # clean and transform rectangular data
library(hrbrthemes) # plot theming

library(corrr) # visualize numeric correlations
  
library(tidymodels) # machine learning tools
library(finetune) # racing methods for accelerating hyperparameter tuning
library(textrecipes) # ml tools for text models

library(themis) # ml prep tools for handling unbalanced datasets
library(baguette) # ml tools for bagged decision tree models
  
library(vip) # interpret model performance
library(SHAPforxgboost) # model explainability
library(DALEXtra) # for model explainability

})

source(here::here("code","_common.R"),
       verbose = FALSE,
       local = knitr::knit_global())

ggplot2::theme_set(theme_jim(base_size = 12))

#create a data directory
data_dir <- here::here("data",Sys.Date())
if (!file.exists(data_dir)) dir.create(data_dir)

# set a competition metric
mset <- metric_set(rmse)

# set the competition name from the web address
competition_name <- "sliced-s01e08-KJSEks"

zipfile <- paste0(data_dir,"/", competition_name, ".zip")

path_export <- here::here("data",Sys.Date(),paste0(competition_name,".csv"))

Import and Exploratory Data Analysis

A quick reminder before downloading the dataset: Go to the web site and accept the competition terms!!!

Import and Skim

We have basic shell commands available to interact with Kaggle here:

# from the Kaggle api https://github.com/Kaggle/kaggle-api

# the leaderboard
shell(glue::glue("kaggle competitions leaderboard { competition_name } -s"))

# the files to download
shell(glue::glue("kaggle competitions files -c { competition_name }"))

# the command to download files
shell(glue::glue("kaggle competitions download -c { competition_name } -p { data_dir }"))

# unzip the files received
shell(glue::glue("unzip { zipfile } -d { data_dir }"))

We are reading in the contents of the three datafiles here, unnesting the id_artists column, joining the artists table to each id of the artists, cleaning the genres text, and finally collapsing the genres back.

artists <-
  read_csv(file = glue::glue(
    {
      data_dir
    },
    "/artists.csv"
  )) %>%
  mutate(genres = str_remove_all(genres, "\\[|\\]|\\'|\\,"))

train_df <-
  read_csv(file = glue::glue(
    {
      data_dir
    },
    "/train.csv"
  )) %>%
  mutate(id_artists = str_remove_all(id_artists, "\\[|\\]|\\'")) %>%
  tidytext::unnest_tokens(id_artists, id_artists, to_lower = FALSE) %>%
  left_join(artists %>% select(id_artists = id, followers, genres)) %>%
  group_by(
    id,
    popularity,
    name,
    artists,
    duration_ms,
    danceability,
    energy,
    key,
    loudness,
    speechiness,
    acousticness,
    instrumentalness,
    liveness,
    valence,
    tempo,
    release_year
  ) %>%
  summarize(
    genres = str_c(genres, collapse = " "),
    followers = mean(followers),
    .groups = "drop"
  )

test_df <-
  read_csv(file = glue::glue(
    {
      data_dir
    },
    "/test.csv"
  )) %>%
  mutate(id_artists = str_remove_all(id_artists, "\\[|\\]|\\'")) %>%
  tidytext::unnest_tokens(id_artists, id_artists, to_lower = FALSE) %>%
  left_join(artists %>% select(id_artists = id, followers, genres)) %>%
  group_by(
    id,
    name,
    artists,
    duration_ms,
    danceability,
    energy,
    key,
    loudness,
    speechiness,
    acousticness,
    instrumentalness,
    liveness,
    valence,
    tempo,
    release_year
  ) %>%
  summarize(
    genres = str_c(genres, collapse = " "),
    followers = mean(followers),
    .groups = "drop"
  )

Some questions to answer here: What features have missing data, and imputations may be required? What does the outcome variable look like, in terms of imbalance?

skimr::skim(train_df)

Outcome variable popularity ranges from 0 to 100. Only followers is missing obvious data. We will take a closer look at the categorical variable levels in a moment.

Field descriptions, from Kaggle:

train id (Unique identifier of track)

name (Name of the song)

popularity (Ranges from 0 to 100)

duration_ms (Integer typically ranging from 200k to 300k)

artists (List of artists mentioned)

id_artists (Ids of mentioned artists)

danceability (Ranges from 0 to 1)

energy (Ranges from 0 to 1)

key (All keys on octave encoded as values ranging from 0 to 11, starting on C as 0, C# as 1 and so on…)

loudness (Float typically ranging from -60 to 0)

speechiness (Ranges from 0 to 1)

acousticness (Ranges from 0 to 1)

instrumentalness (Ranges from 0 to 1)

liveness (Ranges from 0 to 1)

valence (Ranges from 0 to 1)

tempo (Float typically ranging from 50 to 150)

release_year (Year of release)

release_month (Month of year released)

release_day (Day of month released)

artists

id (Id of artist)

followers (Total number of followers of artist)

genres (Genres associated with this artist)

name (Name of artist)

popularity (Popularity of given artist based on all his/her tracks)

Outcome Variable Distribution

Popularity is left skewed with a lot of zero values. Let’s look at the distribution with mean and median:

summarize_popularity <- function(tbl) {
  tbl %>%
    summarize(
      median_popularity = median(popularity),
      n = n(),
      mean_popularity = mean(popularity),
      .groups = "drop"
    ) %>%
    arrange(desc(n))
}

train_df %>%
  summarize_popularity()
train_df %>%
  ggplot(aes(popularity + .1)) +
  geom_histogram(bins = 30) +
  scale_x_log10(labels = scales::pretty_breaks()) +
  labs(
    title = "Spotify Playlist Tracks Popularity Distribution",
    x = "Popularity"
  )

We will work with tree-based machine learning techniques that tolerate more of this than others.

Categorical Feature Plots

train_df %>%
  tidytext::unnest_tokens(genres, genres, to_lower = FALSE) %>%
  group_by(genres = withfreq(genres)) %>%
  summarize_popularity() %>%
  mutate(
    genres = fct_lump(genres, w = n, 12),
    genres = fct_reorder(genres, mean_popularity)
  ) %>%
  slice_max(order_by = n, n = 11) %>%
  ggplot(aes(mean_popularity, genres)) +
  geom_point(aes(size = n)) +
  scale_size_continuous(
    guide = "none",
    range = c(1, 6)
  ) +
  labs(
    x = "Popularity",
    y = "",
    title = "What of the 11 common genres are most popular?",
    subtitle = "Size of points is proportional to frequency in the dataset. Frequency of occurances in (parenthesis)"
  )

train_df %>%
  group_by(name = withfreq(name)) %>%
  summarize_popularity() %>%
  mutate(
    name = fct_lump(name, w = n, 12),
    name = fct_reorder(name, mean_popularity)
  ) %>%
  slice_max(order_by = n, n = 11) %>%
  ggplot(aes(mean_popularity, name)) +
  geom_point(aes(size = n)) +
  scale_size_continuous(
    guide = "none",
    range = c(1, 6)
  ) +
  labs(
    x = "Popularity",
    y = "",
    title = "What of the 11 most common track names are most popular?",
    subtitle = "Size of points is proportional to frequency in the dataset. Frequency of occurances in (parenthesis)"
  )

Numeric Feature Plots

train_numeric <- train_df %>%
  keep(is.numeric) %>%
  colnames()

train_df %>%
  select_at(all_of(train_numeric)) %>%
  select(-id, -popularity) %>%
  pivot_longer(
    cols = everything(),
    names_to = "key",
    values_to = "value"
  ) %>%
  filter(!is.na(value)) %>%
  ggplot(mapping = aes(value,
    after_stat(density),
    fill = key, color = key
  )) +
  geom_histogram(
    position = "identity",
    bins = 30,
    show.legend = FALSE
  ) +
  facet_wrap(~key, scales = "free", ncol = 3) +
  theme(
    plot.subtitle = ggtext::element_textbox_simple(),
    plot.background = element_rect(color = "white")
  ) +
  labs(
    title = "Numeric Feature Histogram Distributions",
    x = "Numeric Feature",
    y = NULL
  )

A facet plot for all numeric features of popularity over time by numeric feature.

train_df %>%
  select(id, popularity, duration_ms:release_year) %>%
  pivot_longer(
    cols = -c(id, popularity, release_year),
    names_to = "key",
    values_to = "value"
  ) %>%
  ggplot(aes(release_year, value, z = popularity)) +
  stat_summary_hex(alpha = 0.9, bins = 30) +
  scale_x_continuous(n.breaks = 3) +
  facet_wrap(~key, scales = "free") +
  labs(
    fill = "Mean popularity",
    title = "Spotify Numeric Features"
  ) +
  theme(legend.position = c(0.9, 0.2))

Numeric Feature Correlations

train_df %>%
  select(danceability:release_year) %>%
  correlate(
    method = "pearson",
    use = "everything"
  ) %>%
  rearrange() %>%
  shave() %>%
  rplot(
    shape = 16,
    print_cor = TRUE,
    legend = TRUE
  ) +
  scale_x_discrete(guide = guide_axis(n.dodge = 2))

Acousticness, loudness, danceability, and release_year are correlated. Acousticness is anti-correlated with energy.

Conclusion:


Preprocessing

The recipe

To move quickly, this basic recipe tunes nothing, takes all of the numeric features, and uses only 50 genres.

basic_rec <-
  recipe(
    popularity ~ duration_ms + danceability + energy + key + loudness + speechiness + acousticness + instrumentalness + liveness + valence + tempo + release_year + followers + genres,
    data = train_df
  ) %>%
  step_tokenize(genres) %>%
  step_tokenfilter(genres, max_tokens = 50) %>%
  step_tf(genres, weight_scheme = "binary") %>%
  step_mutate_at(contains("tf_genres"),
    fn = ~ if_else(. == TRUE, 1, 0)
  ) %>%
  step_impute_median(followers) %>%
  step_log(followers, duration_ms, offset = 1)

dataset for modeling

basic_rec %>%
  #  finalize_recipe(list(num_comp = 2)) %>%
  prep() %>%
  juice()

Cross Validation

We will use 5-fold cross validation and stratify on popularity to build models that are less likely to over-fit the training data.

Proper business modeling practice would holdout a sample from training entirely for assessing model performance. On the Kaggle competitions, the holdout is the “test” itself in the form of the the competitor submission.

set.seed(2021)

(folds <- vfold_cv(train_df, v = 5, strata = popularity))

Machine Learning: Random Forest

Let’s run models in two steps. The first is a simple, fast shallow random forest, to confirm that the model will run and observe feature importance scores. The second will use xgboost. Both use the basic recipe preprocessor for now.

Model Specification

This first model is a bagged tree, where the number of predictors to consider for each split of a tree (i.e., mtry) equals the number of all available predictors. The min_n of 10 means that each tree branch of the 50 decision trees built have at least 10 observations. As a result, the decision trees in the ensemble all are relatively shallow.

(bag_spec <-
  bag_tree(min_n = 10) %>%
  set_engine("rpart", times = 50) %>%
  set_mode("regression"))
Bagged Decision Tree Model Specification (regression)

Main Arguments:
  cost_complexity = 0
  min_n = 10

Engine-Specific Arguments:
  times = 50

Computational engine: rpart 

Parallel backend

To speed up computation we will use a parallel backend.

all_cores <- parallelly::availableCores(omit = 1)
all_cores
system 
    11 
future::plan("multisession", workers = all_cores) # on Windows

Fit and Variable Importance

Lets make a cursory check of the recipe and variable importance, which comes out of rpart for free. This workflow also handles factors without dummies.

bag_wf <-
  workflow() %>%
  add_recipe(basic_rec) %>%
  add_model(bag_spec)

bag_fit <- parsnip::fit(bag_wf, data = train_df)

extract_fit_parsnip(bag_fit)$fit$imp %>%
  mutate(term = fct_reorder(term, value)) %>%
  ggplot(aes(value, term)) +
  geom_point() +
  geom_errorbarh(aes(
    xmin = value - `std.error` / 2,
    xmax = value + `std.error` / 2
  ),
  height = .3
  ) +
  labs(
    title = "Feature Importance",
    x = NULL, y = NULL
  )

augment(bag_fit, train_df) %>%
  select(id, popularity, .pred) %>%
  rmse(truth = popularity, estimate = .pred)

The RMSE here is not a great result. It suggests that for this model, songs where the popularity is near the mean of 27, the prediction yields an error of around 5.8 on the training data. Even worse, the model is likely over-fit and would perform worse on holdout data.

Even so, let’s bank this first submission to Kaggle as-is, and work more with xgboost to do better.

augment(bag_fit, test_df) %>%
  select(id, popularity = .pred) %>%
  write_csv(file = path_export)
shell(glue::glue('kaggle competitions submit -c { competition_name } -f { path_export } -m "First model"'))

Machine Learning: XGBoost Model 1

Model Specification

Let’s start with a boosted model that runs fast and gives an early indication of which hyperparameters make the most difference in model performance.

(xgboost_spec <- boost_tree(
  trees = tune(),
  min_n = tune(),
  learn_rate = tune(),
  tree_depth = tune(),
  stop_iter = 20
) %>%
  set_engine("xgboost", validation = 0.2) %>%
  set_mode("regression"))
Boosted Tree Model Specification (regression)

Main Arguments:
  trees = tune()
  min_n = tune()
  tree_depth = tune()
  learn_rate = tune()
  stop_iter = 20

Engine-Specific Arguments:
  validation = 0.2

Computational engine: xgboost 

Tuning and Performance

The grid here is only 7 combinations of the default parameters and the tune_grid process is setup to stop early.

set.seed(2021)
cv_res_xgboost <-
  workflow() %>%
  add_recipe(basic_rec) %>%
  add_model(xgboost_spec) %>%
  tune_grid(
    resamples = folds,
    grid = 7,
    metrics = mset
  )
autoplot(cv_res_xgboost)

collect_metrics(cv_res_xgboost) %>%
  arrange(mean)

The best learning rates are very small, around 0.013. Setting trees at 753 and depth at 2 appears to give stable results.

Machine Learning: XGBoost Model 2

Let’s use what we learned above to set the learning rate, the number of trees and tree depth, and add back other hyperparameters for further tuning. This time, let’s also try thetune_race_anova technique for skipping the parts of the grid search that do not perform well.

Model Specification

(xgboost_spec <- boost_tree(
  trees = 753,
  min_n = tune(),
  learn_rate = 0.013,
  tree_depth = 2,
  sample_size = tune(),
  loss_reduction = tune(),
  stop_iter = 20
) %>%
  set_engine("xgboost", validation = 0.1) %>%
  set_mode("regression"))
Boosted Tree Model Specification (regression)

Main Arguments:
  trees = 753
  min_n = tune()
  tree_depth = 2
  learn_rate = 0.013
  loss_reduction = tune()
  sample_size = tune()
  stop_iter = 20

Engine-Specific Arguments:
  validation = 0.1

Computational engine: xgboost 
xgboost_param <- parameters(xgboost_spec) %>%
  update(
    min_n = min_n(c(4, 39))
  )

Tuning and Performance

cv_res_xgboost <-
  workflow() %>%
  add_recipe(basic_rec) %>%
  add_model(xgboost_spec) %>%
  tune_race_anova(
    resamples = folds,
    grid = xgboost_param %>% grid_max_entropy(size = 12),
    control = control_race(
      verbose = FALSE,
      save_pred = TRUE,
      save_workflow = TRUE,
      extract = extract_model,
      parallel_over = "resamples"
    ),
    metrics = mset
  )
autoplot(cv_res_xgboost)

collect_metrics(cv_res_xgboost) %>%
  arrange(mean)

The best mean rmse across folds is about 12, which is discouraging. This figure is likely more robust and a better estimate of performance on holdout data. Let’s fit on the entire training set at these hyperparameters to get a single RMSE performance estimate on the best model so far.

xgb_wf_best <-
  workflow() %>%
  add_recipe(basic_rec) %>%
  add_model(xgboost_spec) %>%
  finalize_workflow(select_best(cv_res_xgboost))

fit_best <- xgb_wf_best %>%
  parsnip::fit(data = train_df)

augment(fit_best, train_df) %>%
  select(id, popularity, .pred) %>%
  rmse(truth = popularity, estimate = .pred)

Let’s bank this second submission to Kaggle, and work even more with xgboost to do better.

augment(fit_best, test_df) %>%
  select(id, popularity = .pred) %>%
  write_csv(file = path_export)
shell(glue::glue('kaggle competitions submit -c { competition_name } -f { path_export } -m "Second model"'))

SHAPley Variable Importance

Let’s take a deeper dive into the XGBoost variable importance.

fit_best %>%
  extract_fit_parsnip() %>%
  vip(geom = "point") +
  labs(
    title = "XGBoost model Variable Importance",
    subtitle = "VIP package"
  )

For inference, the XGBoost model suggests that after release_year and followers, the underlying features that drive the popularity are somewhat different. Let’s look closer to understand what is going on to improve the model further.

some_training_data <- basic_rec %>%
  prep() %>%
  bake(new_data = slice_sample(train_df, n = 500), composition = "matrix")

parsnip_fit <- extract_fit_parsnip(fit_best)

# remove the outcome variable
shap <- shap.prep(parsnip_fit$fit, X_train = some_training_data[, -14])

# shap.importance(shap, names_only = TRUE)

shap.plot.summary(shap)

As mentioned before, release_year and the artist’s followers (log scale) have a large influence on popularity in the model output. Let’s look at individual features.

shap.plot.dependence(shap, "release_year",
  color_feature = "auto", alpha = 0.6,
  jitter_width = 0.1
)

The release year SHAP values are almost linear. There may be an interaction with energy that would improve the model.

shap.plot.dependence(shap, "followers",
  color_feature = "auto", alpha = 0.6,
  jitter_width = 0.1
)

The log transformation of followers was justified, though something strange must be happening at 12. There may be an interaction with release_year that could improve the model.

And for the feature instrumentalness:

shap.plot.dependence(shap, "instrumentalness",
  color_feature = "auto", alpha = 0.6,
  jitter_width = 0.02
)

DALEX Partial Dependence Plots

What is the aggregated effect of the release_year feature over 500 examples?

explainer_xgb <- explain_tidymodels(
  fit_best,
  train_df %>% select(-popularity),
  train_df$popularity
)
Preparation of a new explainer is initiated
  -> model label       :  workflow  (  default  )
  -> data              :  21000  rows  17  cols 
  -> data              :  tibble converted into a data.frame 
  -> target variable   :  21000  values 
  -> predict function  :  yhat.workflow  will be used (  default  )
  -> predicted values  :  No value for predict function target column. (  default  )
  -> model_info        :  package tidymodels , ver. 0.1.4 , task regression (  default  ) 
  -> predicted values  :  numerical, min =  -3.274729 , mean =  27.55155 , max =  63.13488  
  -> residual function :  difference between y and yhat (  default  )
  -> residuals         :  numerical, min =  -55.70597 , mean =  0.02830947 , max =  51.18548  
  A new explainer has been created!  
pdp_year <- model_profile(explainer_xgb,
  N = 500,
  variables = "release_year"
)

as_tibble(pdp_year$agr_profiles) %>%
  ggplot(aes(`_x_`, `_yhat_`)) +
  geom_line(
    data = as_tibble(
      pdp_year$cp_profiles
    ),
    aes(release_year, group = `_ids_`),
    size = 0.5, alpha = 0.1, color = "gray30"
  ) +
  geom_line(size = 1.2, alpha = 0.8, color = "orange") +
  labs(x = "Year", y = "Predicted Popularity")

What is the aggregated effect of the followers feature over 500 examples?

pdp_followers <- model_profile(explainer_xgb,
  N = 500,
  variables = "followers"
)

as_tibble(pdp_followers$agr_profiles) %>%
  ggplot(aes(`_x_`, `_yhat_`)) +
  geom_line(
    data = as_tibble(
      pdp_followers$cp_profiles
    ),
    aes(followers, group = `_ids_`),
    size = 0.5, alpha = 0.1, color = "gray30"
  ) +
  geom_line(size = 1.2, alpha = 0.8, color = "darkblue") +
  labs(x = "Artist Followers", y = "Predicted Popularity")

What is the aggregated effect of the acousticness feature over 500 examples?

pdp_acousticness <- model_profile(explainer_xgb,
  N = 500,
  variables = "acousticness"
)

as_tibble(pdp_acousticness$agr_profiles) %>%
  ggplot(aes(`_x_`, `_yhat_`)) +
  geom_line(
    data = as_tibble(
      pdp_acousticness$cp_profiles
    ),
    aes(acousticness, group = `_ids_`),
    size = 0.5, alpha = 0.1, color = "gray30"
  ) +
  geom_line(size = 1.2, alpha = 0.8, color = "green") +
  labs(x = "Artist acousticness", y = "Predicted Popularity")

What is the aggregated effect of the duration feature over 500 examples?

pdp_duration <- model_profile(explainer_xgb,
  N = 500,
  variables = "duration_ms"
)

as_tibble(pdp_duration$agr_profiles) %>%
  ggplot(aes(`_x_`, `_yhat_`)) +
  geom_line(
    data = as_tibble(
      pdp_duration$cp_profiles
    ),
    aes(duration_ms, group = `_ids_`),
    size = 0.5, alpha = 0.1, color = "gray30"
  ) +
  geom_line(size = 1.2, alpha = 0.8, color = "darkgreen") +
  labs(x = "Song Duration (ms)", y = "Predicted Popularity")

Machine Learning XGBoost 3

Let’s add our interactions from above and introduce dimensionality reduction.

Recipe

Principal component analysis (PCA) is a transformation of a group of variables that produces a new set of artificial features or components. These components are designed to capture the maximum amount of information (i.e. variance) in the original variables. Also, the components are statistically independent from one another. This means that they can be used to combat large inter-variable correlations in a data set.

(advanced_rec <-
  basic_rec %>%
  step_interact(terms = ~ release_year:energy) %>%
  step_interact(terms = ~ release_year:followers) %>%
  step_normalize(all_numeric_predictors()) %>%
  step_pca(all_numeric_predictors(), threshold = 0.9))
Recipe

Inputs:

      role #variables
   outcome          1
 predictor         14

Operations:

Tokenization for genres
Text filtering for genres
Term frequency with genres
Variable mutation for contains("tf_genres")
Median Imputation for followers
Log transformation on followers, duration_ms
Interactions with release_year:energy
Interactions with release_year:followers
Centering and scaling for all_numeric_predictors()
No PCA components were extracted.
tidy_coef <- tidy(prep(advanced_rec), 10, type = "coef")

tidy_coef %>%
  filter(component %in% paste0("PC", 1:6)) %>%
  group_by(component) %>%
  slice_max(abs(value), n = 12) %>%
  ungroup() %>%
  mutate(terms = tidytext::reorder_within(terms, abs(value), component)) %>%
  ggplot(aes(abs(value), terms, fill = value > 0)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~component, scales = "free_y") +
  tidytext::scale_y_reordered() +
  labs(
    title = "What features drive the top 6 principal components?",
    subtitle = "Note the presence of the interactions. Recent, high energy, loud tracks are most popular.",
    y = NULL
  )

bake(prep(advanced_rec), new_data = NULL) %>%
  ggplot(aes(PC01, PC02, z = popularity)) +
  stat_summary_hex(alpha = 0.9, bins = 40) +
  scale_fill_viridis_c(option = "E") +
  labs(
    fill = "Popularity",
    title = "First two principal components"
  )

Tuning and Performance

The clean advanced preprocessor with interactions should yield a better model. Let’s see.

(xgboost_spec <- boost_tree(
  trees = tune(),
  min_n = tune(),
  learn_rate = 0.02,
  tree_depth = tune(),
  sample_size = tune(),
  loss_reduction = tune()
) %>%
  set_engine("xgboost") %>%
  set_mode("regression"))
Boosted Tree Model Specification (regression)

Main Arguments:
  trees = tune()
  min_n = tune()
  tree_depth = tune()
  learn_rate = 0.02
  loss_reduction = tune()
  sample_size = tune()

Computational engine: xgboost 
cv_res_xgboost <-
  workflow() %>%
  add_recipe(advanced_rec) %>%
  add_model(xgboost_spec) %>%
  tune_race_anova(
    resamples = folds,
    grid = 10,
    control = control_race(
      verbose = FALSE,
      save_pred = TRUE,
      save_workflow = TRUE,
      extract = extract_model,
      parallel_over = "resamples"
    ),
    metrics = mset
  )
autoplot(cv_res_xgboost)

collect_metrics(cv_res_xgboost) %>%
  arrange(mean)

The best mean rmse averaged across folds is about 12.1.

xgb_wf_best <-
  workflow() %>%
  add_recipe(advanced_rec) %>%
  add_model(xgboost_spec) %>%
  finalize_workflow(select_best(cv_res_xgboost))

fit_best <- xgb_wf_best %>%
  parsnip::fit(data = train_df)

augment(fit_best, train_df) %>%
  select(id, popularity, .pred) %>%
  rmse(truth = popularity, estimate = .pred)

We’re out of time. This will be as good as it gets. Our final submission:

augment(fit_best, test_df) %>%
  select(id, popularity = .pred) %>%
  write_csv(file = path_export)
shell(glue::glue('kaggle competitions submit -c { competition_name } -f { path_export } -m "Third model"'))


It turns out that an RMSE in the neighbornood of 10 is competitive with the others on the leaderboard.


sessionInfo()
R version 4.1.1 (2021-08-10)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 22000)

Matrix products: default

locale:
[1] LC_COLLATE=English_United States.1252 
[2] LC_CTYPE=English_United States.1252   
[3] LC_MONETARY=English_United States.1252
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.1252    

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

other attached packages:
 [1] xgboost_1.4.1.1      vctrs_0.3.8          rlang_0.4.11        
 [4] DALEXtra_2.1.1       DALEX_2.3.0          SHAPforxgboost_0.1.1
 [7] vip_0.3.2            baguette_0.1.1       themis_0.1.4        
[10] textrecipes_0.4.1    finetune_0.1.0       yardstick_0.0.8     
[13] workflowsets_0.1.0   workflows_0.2.3      tune_0.1.6          
[16] rsample_0.1.0        recipes_0.1.17       parsnip_0.1.7.900   
[19] modeldata_0.1.1      infer_1.0.0          dials_0.0.10        
[22] scales_1.1.1         broom_0.7.9          tidymodels_0.1.4    
[25] corrr_0.4.3          hrbrthemes_0.8.0     forcats_0.5.1       
[28] stringr_1.4.0        dplyr_1.0.7          purrr_0.3.4         
[31] readr_2.0.2          tidyr_1.1.4          tibble_3.1.4        
[34] ggplot2_3.3.5        tidyverse_1.3.1      workflowr_1.6.2     

loaded via a namespace (and not attached):
  [1] rappdirs_0.3.3     SnowballC_0.7.0    R.methodsS3_1.8.1 
  [4] earth_5.3.1        ragg_1.1.3         bit64_4.0.5       
  [7] knitr_1.36         R.utils_2.11.0     styler_1.6.2      
 [10] data.table_1.14.2  rpart_4.1-15       hardhat_0.1.6     
 [13] doParallel_1.0.16  generics_0.1.0     GPfit_1.0-8       
 [16] usethis_2.0.1      RANN_2.6.1         future_1.22.1     
 [19] conflicted_1.0.4   bit_4.0.4          tzdb_0.1.2        
 [22] tokenizers_0.2.1   xml2_1.3.2         lubridate_1.7.10  
 [25] httpuv_1.6.3       assertthat_0.2.1   viridis_0.6.1     
 [28] gower_0.2.2        xfun_0.26          hms_1.1.1         
 [31] jquerylib_0.1.4    TSP_1.1-10         evaluate_0.14     
 [34] promises_1.2.0.1   fansi_0.5.0        dbplyr_2.1.1      
 [37] readxl_1.3.1       DBI_1.1.1          ellipsis_0.3.2    
 [40] ggpubr_0.4.0       backports_1.2.1    libcoin_1.0-9     
 [43] here_1.0.1         abind_1.4-5        cachem_1.0.6      
 [46] withr_2.4.2        ggforce_0.3.3      checkmate_2.0.0   
 [49] vroom_1.5.5        lime_0.5.2         crayon_1.4.1      
 [52] glmnet_4.1-2       pkgconfig_2.0.3    labeling_0.4.2    
 [55] tweenr_1.0.2       nlme_3.1-152       seriation_1.3.0   
 [58] nnet_7.3-16        globals_0.14.0     lifecycle_1.0.1   
 [61] registry_0.5-1     extrafontdb_1.0    ingredients_2.2.0 
 [64] unbalanced_2.0     modelr_0.1.8       tidytext_0.3.2    
 [67] cellranger_1.1.0   Cubist_0.3.0       rprojroot_2.0.2   
 [70] polyclip_1.10-0    partykit_1.2-15    Matrix_1.3-4      
 [73] carData_3.0-4      reprex_2.0.1       whisker_0.4       
 [76] png_0.1-7          viridisLite_0.4.0  ROSE_0.0-4        
 [79] R.oo_1.24.0        pROC_1.18.0        mlr_2.19.0        
 [82] shape_1.4.6        parallelly_1.28.1  R.cache_0.15.0    
 [85] rstatix_0.7.0      ggsignif_0.6.3     hexbin_1.28.2     
 [88] magrittr_2.0.1     ParamHelpers_1.14  plyr_1.8.6        
 [91] compiler_4.1.1     RColorBrewer_1.1-2 plotrix_3.8-2     
 [94] cli_3.0.1          DiceDesign_1.9     listenv_0.8.0     
 [97] janeaustenr_0.1.5  Formula_1.2-4      mgcv_1.8-36       
[100] MASS_7.3-54        tidyselect_1.1.1   stringi_1.7.5     
[103] textshaping_0.3.5  butcher_0.1.5      highr_0.9         
[106] yaml_2.2.1         grid_4.1.1         sass_0.4.0        
[109] fastmatch_1.1-3    tools_4.1.1        future.apply_1.8.1
[112] parallel_4.1.1     rio_0.5.27         rstudioapi_0.13   
[115] foreach_1.5.1      foreign_0.8-81     inum_1.0-4        
[118] git2r_0.28.0       gridExtra_2.3      prodlim_2019.11.13
[121] C50_0.1.5          farver_2.1.0       digest_0.6.28     
[124] FNN_1.1.3          ggtext_0.1.1       lava_1.6.10       
[127] TeachingDemos_2.12 gridtext_0.1.4     Rcpp_1.0.7        
[130] car_3.0-11         later_1.3.0        httr_1.4.2        
[133] gdtools_0.2.3      colorspace_2.0-2   rvest_1.0.1       
[136] fs_1.5.0           reticulate_1.22    splines_4.1.1     
[139] rematch2_2.1.2     systemfonts_1.0.2  jsonlite_1.7.2    
[142] BBmisc_1.11        timeDate_3043.102  plotmo_3.6.1      
[145] ipred_0.9-12       R6_2.5.1           lhs_1.1.3         
[148] pillar_1.6.3       htmltools_0.5.2    glue_1.4.2        
[151] fastmap_1.1.0      class_7.3-19       codetools_0.2-18  
[154] mvtnorm_1.1-2      furrr_0.2.3        utf8_1.2.2        
[157] lattice_0.20-44    bslib_0.3.0        curl_4.3.2        
[160] zip_2.2.0          openxlsx_4.2.4     Rttf2pt1_1.3.8    
[163] survival_3.2-11    parallelMap_1.5.1  rmarkdown_2.11    
[166] munsell_0.5.0      iterators_1.0.13   haven_2.4.3       
[169] reshape2_1.4.4     gtable_0.3.0       extrafont_0.17    
LS0tDQp0aXRsZTogIlNsaWNlZCBTcG90aWZ5IFRyYWNrIFBvcHVsYXJpdHkiDQphdXRob3I6ICJKaW0gR3J1bWFuIg0KZGF0ZTogIkp1bHkgMjAsIDIwMjEiDQpvdXRwdXQ6DQogIHdvcmtmbG93cjo6d2Zsb3dfaHRtbDoNCiAgICB0b2M6IG5vDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGRmX3ByaW50OiBwYWdlZA0KZWRpdG9yX29wdGlvbnM6DQogIGNodW5rX291dHB1dF90eXBlOiBjb25zb2xlDQotLS0NCg0KW1NlYXNvbiAxIEVwaXNvZGUgOF0oaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9jL3NsaWNlZC1zMDFlMDgtS0pTRWtzL2RhdGEpIGZlYXR1cmVzIGEgY2hhbGxlbmdlIHRvIHByZWRpY3QgdGhlIHBvcHVsYXJpdHkgb2YgU3BvdGlmeSB0cmFja3MuIFRoZSBldmFsdWF0aW9uIG1ldHJpYyBmb3Igc3VibWlzc2lvbnMgaW4gdGhpcyBjb21wZXRpdGlvbiBpcyByZXNpZHVhbCBtZWFuIHNxdWFyZWQgZXJyb3IuDQoNCiFbXShodHRwczovL3d3dy5ub3Rpb24uc28vaW1hZ2UvaHR0cHMlM0ElMkYlMkZzMy11cy13ZXN0LTIuYW1hem9uYXdzLmNvbSUyRnNlY3VyZS5ub3Rpb24tc3RhdGljLmNvbSUyRjdmN2JhNWY5LWQ3YmQtNDEwMS04OTMzLWExMTJiNGY3ODU3MCUyRkZyYW1lXzMucG5nP3RhYmxlPWJsb2NrJmlkPWM3YmQyNjM1LTZlM2EtNDIyNy05ZTJkLWZiYWZiMDQ4MDA3MyZzcGFjZUlkPTJjYzQwNGU2LWZlMjAtNDgzZC05ZWE1LTVkNDRlYjNkZDU4NiZ3aWR0aD0xNTEwJnVzZXJJZD0mY2FjaGU9djIpDQoNCltTTElDRURdKGh0dHBzOi8vd3d3Lm5vdGlvbi5zby9TTElDRUQtU2hvdy1jN2JkMjYzNTZlM2E0MjI3OWUyZGZiYWZiMDQ4MDA3MykgaXMgbGlrZSB0aGUgVFYgU2hvdyBDaG9wcGVkIGJ1dCBmb3IgZGF0YSBzY2llbmNlLiBDb21wZXRpdG9ycyBnZXQgYSBuZXZlci1iZWZvcmUtc2VlbiBkYXRhc2V0IGFuZCB0d28taG91cnMgdG8gY29kZSBhIHNvbHV0aW9uIHRvIGEgcHJlZGljdGlvbiBjaGFsbGVuZ2UuIENvbnRlc3RhbnRzIGdldCBwb2ludHMgZm9yIHRoZSBiZXN0IG1vZGVsIHBsdXMgYm9udXMgcG9pbnRzIGZvciBkYXRhIHZpc3VhbGl6YXRpb24sIHZvdGVzIGZyb20gdGhlIGF1ZGllbmNlLCBhbmQgbW9yZS4NCg0KVGhlIGF1ZGllbmNlIGlzIGludml0ZWQgdG8gcGFydGljaXBhdGUgYXMgd2VsbC4gVGhpcyBmaWxlIGNvbnNpc3RzIG9mIG15IHN1Ym1pc3Npb25zIHdpdGggY2xlYW51cCBhbmQgY29tbWVudGFyeSBhZGRlZC4NCg0KVG8gbWFrZSB0aGUgYmVzdCB1c2Ugb2YgdGhlIHJlc291cmNlcyB0aGF0IHdlIGhhdmUsIHdlIHdpbGwgZXhwbG9yZSB0aGUgZGF0YSBzZXQgZm9yIGZlYXR1cmVzIHRvIHNlbGVjdCB0aG9zZSB3aXRoIHRoZSBtb3N0IHByZWRpY3RpdmUgcG93ZXIsIGJ1aWxkIGEgcmFuZG9tIGZvcmVzdCB0byBjb25maXJtIHRoZSByZWNpcGUsIGFuZCB0aGVuIGJ1aWxkIG9uZSBvciBtb3JlIGVuc2VtYmxlIG1vZGVscy4gSWYgdGhlcmUgaXMgdGltZSwgd2Ugd2lsbCBjcmFmdCBzb21lIHZpc3VhbHMgZm9yIG1vZGVsIGV4cGxhaW5hYmlsaXR5Lg0KDQpMZXQncyBsb2FkIHVwIHBhY2thZ2VzOg0KDQpgYGB7ciBzZXR1cH0NCg0Kc3VwcHJlc3NQYWNrYWdlU3RhcnR1cE1lc3NhZ2VzKHsNCmxpYnJhcnkodGlkeXZlcnNlKSAjIGNsZWFuIGFuZCB0cmFuc2Zvcm0gcmVjdGFuZ3VsYXIgZGF0YQ0KbGlicmFyeShocmJydGhlbWVzKSAjIHBsb3QgdGhlbWluZw0KDQpsaWJyYXJ5KGNvcnJyKSAjIHZpc3VhbGl6ZSBudW1lcmljIGNvcnJlbGF0aW9ucw0KICANCmxpYnJhcnkodGlkeW1vZGVscykgIyBtYWNoaW5lIGxlYXJuaW5nIHRvb2xzDQpsaWJyYXJ5KGZpbmV0dW5lKSAjIHJhY2luZyBtZXRob2RzIGZvciBhY2NlbGVyYXRpbmcgaHlwZXJwYXJhbWV0ZXIgdHVuaW5nDQpsaWJyYXJ5KHRleHRyZWNpcGVzKSAjIG1sIHRvb2xzIGZvciB0ZXh0IG1vZGVscw0KDQpsaWJyYXJ5KHRoZW1pcykgIyBtbCBwcmVwIHRvb2xzIGZvciBoYW5kbGluZyB1bmJhbGFuY2VkIGRhdGFzZXRzDQpsaWJyYXJ5KGJhZ3VldHRlKSAjIG1sIHRvb2xzIGZvciBiYWdnZWQgZGVjaXNpb24gdHJlZSBtb2RlbHMNCiAgDQpsaWJyYXJ5KHZpcCkgIyBpbnRlcnByZXQgbW9kZWwgcGVyZm9ybWFuY2UNCmxpYnJhcnkoU0hBUGZvcnhnYm9vc3QpICMgbW9kZWwgZXhwbGFpbmFiaWxpdHkNCmxpYnJhcnkoREFMRVh0cmEpICMgZm9yIG1vZGVsIGV4cGxhaW5hYmlsaXR5DQoNCn0pDQoNCnNvdXJjZShoZXJlOjpoZXJlKCJjb2RlIiwiX2NvbW1vbi5SIiksDQogICAgICAgdmVyYm9zZSA9IEZBTFNFLA0KICAgICAgIGxvY2FsID0ga25pdHI6OmtuaXRfZ2xvYmFsKCkpDQoNCmdncGxvdDI6OnRoZW1lX3NldCh0aGVtZV9qaW0oYmFzZV9zaXplID0gMTIpKQ0KDQojY3JlYXRlIGEgZGF0YSBkaXJlY3RvcnkNCmRhdGFfZGlyIDwtIGhlcmU6OmhlcmUoImRhdGEiLFN5cy5EYXRlKCkpDQppZiAoIWZpbGUuZXhpc3RzKGRhdGFfZGlyKSkgZGlyLmNyZWF0ZShkYXRhX2RpcikNCg0KIyBzZXQgYSBjb21wZXRpdGlvbiBtZXRyaWMNCm1zZXQgPC0gbWV0cmljX3NldChybXNlKQ0KDQojIHNldCB0aGUgY29tcGV0aXRpb24gbmFtZSBmcm9tIHRoZSB3ZWIgYWRkcmVzcw0KY29tcGV0aXRpb25fbmFtZSA8LSAic2xpY2VkLXMwMWUwOC1LSlNFa3MiDQoNCnppcGZpbGUgPC0gcGFzdGUwKGRhdGFfZGlyLCIvIiwgY29tcGV0aXRpb25fbmFtZSwgIi56aXAiKQ0KDQpwYXRoX2V4cG9ydCA8LSBoZXJlOjpoZXJlKCJkYXRhIixTeXMuRGF0ZSgpLHBhc3RlMChjb21wZXRpdGlvbl9uYW1lLCIuY3N2IikpDQpgYGANCg0KIyBJbXBvcnQgYW5kIEV4cGxvcmF0b3J5IERhdGEgQW5hbHlzaXMgey50YWJzZXR9DQoNCkEgcXVpY2sgcmVtaW5kZXIgYmVmb3JlIGRvd25sb2FkaW5nIHRoZSBkYXRhc2V0OiAgR28gdG8gdGhlIHdlYiBzaXRlIGFuZCBhY2NlcHQgdGhlIGNvbXBldGl0aW9uIHRlcm1zISEhDQoNCiMjIEltcG9ydCBhbmQgU2tpbQ0KDQpXZSBoYXZlIGJhc2ljIHNoZWxsIGNvbW1hbmRzIGF2YWlsYWJsZSB0byBpbnRlcmFjdCB3aXRoIEthZ2dsZSBoZXJlOg0KDQpgYGB7ciBrYWdnbGUgY29tcGV0aXRpb25zIHRlcm1pbmFsIGNvbW1hbmRzLCBldmFsPUZBTFNFfQ0KIyBmcm9tIHRoZSBLYWdnbGUgYXBpIGh0dHBzOi8vZ2l0aHViLmNvbS9LYWdnbGUva2FnZ2xlLWFwaQ0KDQojIHRoZSBsZWFkZXJib2FyZA0Kc2hlbGwoZ2x1ZTo6Z2x1ZSgna2FnZ2xlIGNvbXBldGl0aW9ucyBsZWFkZXJib2FyZCB7IGNvbXBldGl0aW9uX25hbWUgfSAtcycpKQ0KDQojIHRoZSBmaWxlcyB0byBkb3dubG9hZA0Kc2hlbGwoZ2x1ZTo6Z2x1ZSgna2FnZ2xlIGNvbXBldGl0aW9ucyBmaWxlcyAtYyB7IGNvbXBldGl0aW9uX25hbWUgfScpKQ0KDQojIHRoZSBjb21tYW5kIHRvIGRvd25sb2FkIGZpbGVzDQpzaGVsbChnbHVlOjpnbHVlKCdrYWdnbGUgY29tcGV0aXRpb25zIGRvd25sb2FkIC1jIHsgY29tcGV0aXRpb25fbmFtZSB9IC1wIHsgZGF0YV9kaXIgfScpKQ0KDQojIHVuemlwIHRoZSBmaWxlcyByZWNlaXZlZA0Kc2hlbGwoZ2x1ZTo6Z2x1ZSgndW56aXAgeyB6aXBmaWxlIH0gLWQgeyBkYXRhX2RpciB9JykpDQoNCmBgYA0KDQpXZSBhcmUgcmVhZGluZyBpbiB0aGUgY29udGVudHMgb2YgdGhlIHRocmVlIGRhdGFmaWxlcyBoZXJlLCB1bm5lc3RpbmcgdGhlIGlkX2FydGlzdHMgY29sdW1uLCBqb2luaW5nIHRoZSBhcnRpc3RzIHRhYmxlIHRvIGVhY2ggaWQgb2YgdGhlIGFydGlzdHMsIGNsZWFuaW5nIHRoZSBnZW5yZXMgdGV4dCwgYW5kIGZpbmFsbHkgY29sbGFwc2luZyB0aGUgZ2VucmVzIGJhY2suDQoNCmBgYHtyIHJlYWQga2FnZ2xlIGZpbGVzfQ0KYXJ0aXN0cyA8LQ0KICByZWFkX2NzdihmaWxlID0gZ2x1ZTo6Z2x1ZSh7DQogICAgZGF0YV9kaXINCiAgfSwgIi9hcnRpc3RzLmNzdiIpKSAlPiUNCiAgbXV0YXRlKGdlbnJlcyA9IHN0cl9yZW1vdmVfYWxsKGdlbnJlcywgIlxcW3xcXF18XFwnfFxcLCIpKQ0KDQp0cmFpbl9kZiA8LQ0KICByZWFkX2NzdihmaWxlID0gZ2x1ZTo6Z2x1ZSh7DQogICAgZGF0YV9kaXINCiAgfSwgIi90cmFpbi5jc3YiKSkgJT4lDQogIG11dGF0ZShpZF9hcnRpc3RzID0gc3RyX3JlbW92ZV9hbGwoaWRfYXJ0aXN0cywgIlxcW3xcXF18XFwnIikpICU+JQ0KICB0aWR5dGV4dDo6dW5uZXN0X3Rva2VucyhpZF9hcnRpc3RzLCBpZF9hcnRpc3RzLCB0b19sb3dlciA9IEZBTFNFKSAlPiUNCiAgbGVmdF9qb2luKGFydGlzdHMgJT4lIHNlbGVjdChpZF9hcnRpc3RzID0gaWQsIGZvbGxvd2VycywgZ2VucmVzKSkgJT4lDQogIGdyb3VwX2J5KA0KICAgIGlkLA0KICAgIHBvcHVsYXJpdHksDQogICAgbmFtZSwNCiAgICBhcnRpc3RzLA0KICAgIGR1cmF0aW9uX21zLA0KICAgIGRhbmNlYWJpbGl0eSwNCiAgICBlbmVyZ3ksDQogICAga2V5LA0KICAgIGxvdWRuZXNzLA0KICAgIHNwZWVjaGluZXNzLA0KICAgIGFjb3VzdGljbmVzcywNCiAgICBpbnN0cnVtZW50YWxuZXNzLA0KICAgIGxpdmVuZXNzLA0KICAgIHZhbGVuY2UsDQogICAgdGVtcG8sDQogICAgcmVsZWFzZV95ZWFyDQogICkgJT4lDQogIHN1bW1hcml6ZSgNCiAgICBnZW5yZXMgPSBzdHJfYyhnZW5yZXMsIGNvbGxhcHNlID0gIiAiKSwNCiAgICBmb2xsb3dlcnMgPSBtZWFuKGZvbGxvd2VycyksDQogICAgLmdyb3VwcyA9ICJkcm9wIg0KICApDQoNCnRlc3RfZGYgPC0NCiAgcmVhZF9jc3YoZmlsZSA9IGdsdWU6OmdsdWUoew0KICAgIGRhdGFfZGlyDQogIH0sICIvdGVzdC5jc3YiKSkgJT4lDQogIG11dGF0ZShpZF9hcnRpc3RzID0gc3RyX3JlbW92ZV9hbGwoaWRfYXJ0aXN0cywgIlxcW3xcXF18XFwnIikpICU+JQ0KICB0aWR5dGV4dDo6dW5uZXN0X3Rva2VucyhpZF9hcnRpc3RzLCBpZF9hcnRpc3RzLCB0b19sb3dlciA9IEZBTFNFKSAlPiUNCiAgbGVmdF9qb2luKGFydGlzdHMgJT4lIHNlbGVjdChpZF9hcnRpc3RzID0gaWQsIGZvbGxvd2VycywgZ2VucmVzKSkgJT4lDQogIGdyb3VwX2J5KA0KICAgIGlkLA0KICAgIG5hbWUsDQogICAgYXJ0aXN0cywNCiAgICBkdXJhdGlvbl9tcywNCiAgICBkYW5jZWFiaWxpdHksDQogICAgZW5lcmd5LA0KICAgIGtleSwNCiAgICBsb3VkbmVzcywNCiAgICBzcGVlY2hpbmVzcywNCiAgICBhY291c3RpY25lc3MsDQogICAgaW5zdHJ1bWVudGFsbmVzcywNCiAgICBsaXZlbmVzcywNCiAgICB2YWxlbmNlLA0KICAgIHRlbXBvLA0KICAgIHJlbGVhc2VfeWVhcg0KICApICU+JQ0KICBzdW1tYXJpemUoDQogICAgZ2VucmVzID0gc3RyX2MoZ2VucmVzLCBjb2xsYXBzZSA9ICIgIiksDQogICAgZm9sbG93ZXJzID0gbWVhbihmb2xsb3dlcnMpLA0KICAgIC5ncm91cHMgPSAiZHJvcCINCiAgKQ0KYGBgDQoNClNvbWUgcXVlc3Rpb25zIHRvIGFuc3dlciBoZXJlOg0KV2hhdCBmZWF0dXJlcyBoYXZlIG1pc3NpbmcgZGF0YSwgYW5kIGltcHV0YXRpb25zIG1heSBiZSByZXF1aXJlZD8NCldoYXQgZG9lcyB0aGUgb3V0Y29tZSB2YXJpYWJsZSBsb29rIGxpa2UsIGluIHRlcm1zIG9mIGltYmFsYW5jZT8NCg0KYGBge3Igc2tpbSwgZXZhbD1GQUxTRX0NCnNraW1yOjpza2ltKHRyYWluX2RmKQ0KYGBgDQoNCk91dGNvbWUgdmFyaWFibGUgYHBvcHVsYXJpdHlgIHJhbmdlcyBmcm9tIDAgdG8gMTAwLiBPbmx5IGBmb2xsb3dlcnNgIGlzIG1pc3Npbmcgb2J2aW91cyBkYXRhLiBXZSB3aWxsIHRha2UgYSBjbG9zZXIgbG9vayBhdCB0aGUgY2F0ZWdvcmljYWwgdmFyaWFibGUgbGV2ZWxzIGluIGEgbW9tZW50Lg0KDQpGaWVsZCBkZXNjcmlwdGlvbnMsIGZyb20gS2FnZ2xlOg0KDQoqKnRyYWluKioNCmBpZGAgKFVuaXF1ZSBpZGVudGlmaWVyIG9mIHRyYWNrKQ0KDQpgbmFtZWAgKE5hbWUgb2YgdGhlIHNvbmcpDQoNCmBwb3B1bGFyaXR5YCAoUmFuZ2VzIGZyb20gMCB0byAxMDApDQoNCmBkdXJhdGlvbl9tc2AgKEludGVnZXIgdHlwaWNhbGx5IHJhbmdpbmcgZnJvbSAyMDBrIHRvIDMwMGspDQoNCmBhcnRpc3RzYCAoTGlzdCBvZiBhcnRpc3RzIG1lbnRpb25lZCkNCg0KYGlkX2FydGlzdHNgIChJZHMgb2YgbWVudGlvbmVkIGFydGlzdHMpDQoNCmBkYW5jZWFiaWxpdHlgIChSYW5nZXMgZnJvbSAwIHRvIDEpDQoNCmBlbmVyZ3lgIChSYW5nZXMgZnJvbSAwIHRvIDEpDQoNCmBrZXlgIChBbGwga2V5cyBvbiBvY3RhdmUgZW5jb2RlZCBhcyB2YWx1ZXMgcmFuZ2luZyBmcm9tIDAgdG8gMTEsIHN0YXJ0aW5nIG9uIEMgYXMgMCwgQyMgYXMgMSBhbmQgc28gb24uLi4pDQoNCmBsb3VkbmVzc2AgKEZsb2F0IHR5cGljYWxseSByYW5naW5nIGZyb20gLTYwIHRvIDApDQoNCmBzcGVlY2hpbmVzc2AgKFJhbmdlcyBmcm9tIDAgdG8gMSkNCg0KYGFjb3VzdGljbmVzc2AgKFJhbmdlcyBmcm9tIDAgdG8gMSkNCg0KYGluc3RydW1lbnRhbG5lc3NgIChSYW5nZXMgZnJvbSAwIHRvIDEpDQoNCmBsaXZlbmVzc2AgKFJhbmdlcyBmcm9tIDAgdG8gMSkNCg0KYHZhbGVuY2VgIChSYW5nZXMgZnJvbSAwIHRvIDEpDQoNCmB0ZW1wb2AgKEZsb2F0IHR5cGljYWxseSByYW5naW5nIGZyb20gNTAgdG8gMTUwKQ0KDQpgcmVsZWFzZV95ZWFyYCAoWWVhciBvZiByZWxlYXNlKQ0KDQpgcmVsZWFzZV9tb250aGAgKE1vbnRoIG9mIHllYXIgcmVsZWFzZWQpDQoNCmByZWxlYXNlX2RheWAgKERheSBvZiBtb250aCByZWxlYXNlZCkNCg0KKiphcnRpc3RzKioNCg0KYGlkYCAoSWQgb2YgYXJ0aXN0KQ0KDQpgZm9sbG93ZXJzYCAoVG90YWwgbnVtYmVyIG9mIGZvbGxvd2VycyBvZiBhcnRpc3QpDQoNCmBnZW5yZXNgIChHZW5yZXMgYXNzb2NpYXRlZCB3aXRoIHRoaXMgYXJ0aXN0KQ0KDQpgbmFtZWAgKE5hbWUgb2YgYXJ0aXN0KQ0KDQpgcG9wdWxhcml0eWAgKFBvcHVsYXJpdHkgb2YgZ2l2ZW4gYXJ0aXN0IGJhc2VkIG9uIGFsbCBoaXMvaGVyIHRyYWNrcykNCg0KIyMgT3V0Y29tZSBWYXJpYWJsZSBEaXN0cmlidXRpb24NCg0KUG9wdWxhcml0eSBpcyBsZWZ0IHNrZXdlZCB3aXRoIGEgbG90IG9mIHplcm8gdmFsdWVzLiBMZXQncyBsb29rIGF0IHRoZSBkaXN0cmlidXRpb24gd2l0aCBtZWFuIGFuZCBtZWRpYW46DQoNCmBgYHtyIHN1bW1hcml6ZSBvdXRjb21lfQ0Kc3VtbWFyaXplX3BvcHVsYXJpdHkgPC0gZnVuY3Rpb24odGJsKXsNCiAgdGJsICU+JSANCiAgICBzdW1tYXJpemUobWVkaWFuX3BvcHVsYXJpdHkgPSBtZWRpYW4ocG9wdWxhcml0eSksDQogICAgICAgICAgICAgIG4gPSBuKCksDQogICAgICAgICAgICAgIG1lYW5fcG9wdWxhcml0eSA9IG1lYW4ocG9wdWxhcml0eSksDQogICAgICAgICAgICAgIC5ncm91cHMgPSAiZHJvcCIpICU+JSANCiAgICBhcnJhbmdlKGRlc2MobikpDQp9DQoNCnRyYWluX2RmICU+JQ0KICBzdW1tYXJpemVfcG9wdWxhcml0eSgpDQoNCnRyYWluX2RmICU+JSANCiAgZ2dwbG90KGFlcyhwb3B1bGFyaXR5ICsgLjEgKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShiaW5zID0gMzApICsNCiAgc2NhbGVfeF9sb2cxMChsYWJlbHMgPSBzY2FsZXM6OnByZXR0eV9icmVha3MoKSkgKw0KICBsYWJzKHRpdGxlID0gIlNwb3RpZnkgUGxheWxpc3QgVHJhY2tzIFBvcHVsYXJpdHkgRGlzdHJpYnV0aW9uIiwNCiAgICAgICB4ID0gIlBvcHVsYXJpdHkiKQ0KYGBgDQoNCldlIHdpbGwgd29yayB3aXRoIHRyZWUtYmFzZWQgbWFjaGluZSBsZWFybmluZyB0ZWNobmlxdWVzIHRoYXQgdG9sZXJhdGUgbW9yZSBvZiB0aGlzIHRoYW4gb3RoZXJzLg0KDQojIyBDYXRlZ29yaWNhbCBGZWF0dXJlIFBsb3RzDQoNCmBgYHtyIGNhdGVnb3JpY2FsIGZlYXR1cmUgcGxvdHMsIGZpZy5hc3A9MX0NCnRyYWluX2RmICU+JQ0KICB0aWR5dGV4dDo6dW5uZXN0X3Rva2VucyhnZW5yZXMsIGdlbnJlcywgdG9fbG93ZXIgPSBGQUxTRSkgJT4lIA0KICBncm91cF9ieShnZW5yZXMgPSB3aXRoZnJlcShnZW5yZXMpKSAlPiUNCiAgc3VtbWFyaXplX3BvcHVsYXJpdHkoKSAlPiUgDQogIG11dGF0ZShnZW5yZXMgPSBmY3RfbHVtcChnZW5yZXMsIHcgPSBuLCAxMiksDQogICAgICAgICBnZW5yZXMgPSBmY3RfcmVvcmRlcihnZW5yZXMsIG1lYW5fcG9wdWxhcml0eSkpICU+JSANCiAgc2xpY2VfbWF4KG9yZGVyX2J5ID0gbiwgbiA9IDExKSAlPiUgDQogIGdncGxvdChhZXMobWVhbl9wb3B1bGFyaXR5LCBnZW5yZXMpKSArDQogIGdlb21fcG9pbnQoYWVzKHNpemUgPSBuKSkgKw0KICBzY2FsZV9zaXplX2NvbnRpbnVvdXMoZ3VpZGUgPSAibm9uZSIsDQogICAgICAgICAgICAgICAgICAgICAgICByYW5nZSA9IGMoMSwgNikpICsNCiAgbGFicygNCiAgICB4ID0gIlBvcHVsYXJpdHkiLA0KICAgIHkgPSAiIiwNCiAgICB0aXRsZSA9ICJXaGF0IG9mIHRoZSAxMSBjb21tb24gZ2VucmVzIGFyZSBtb3N0IHBvcHVsYXI/IiwNCiAgICBzdWJ0aXRsZSA9ICJTaXplIG9mIHBvaW50cyBpcyBwcm9wb3J0aW9uYWwgdG8gZnJlcXVlbmN5IGluIHRoZSBkYXRhc2V0LiBGcmVxdWVuY3kgb2Ygb2NjdXJhbmNlcyBpbiAocGFyZW50aGVzaXMpIg0KICApDQoNCnRyYWluX2RmICU+JQ0KICBncm91cF9ieShuYW1lID0gd2l0aGZyZXEobmFtZSkpICU+JQ0KICBzdW1tYXJpemVfcG9wdWxhcml0eSgpICU+JSANCiAgbXV0YXRlKG5hbWUgPSBmY3RfbHVtcChuYW1lLCB3ID0gbiwgMTIpLA0KICAgICAgICAgbmFtZSA9IGZjdF9yZW9yZGVyKG5hbWUsIG1lYW5fcG9wdWxhcml0eSkpICU+JSANCiAgc2xpY2VfbWF4KG9yZGVyX2J5ID0gbiwgbiA9IDExKSAlPiUgDQogIGdncGxvdChhZXMobWVhbl9wb3B1bGFyaXR5LCBuYW1lKSkgKw0KICBnZW9tX3BvaW50KGFlcyhzaXplID0gbikpICsNCiAgc2NhbGVfc2l6ZV9jb250aW51b3VzKGd1aWRlID0gIm5vbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgcmFuZ2UgPSBjKDEsIDYpKSArDQogIGxhYnMoDQogICAgeCA9ICJQb3B1bGFyaXR5IiwNCiAgICB5ID0gIiIsDQogICAgdGl0bGUgPSAiV2hhdCBvZiB0aGUgMTEgbW9zdCBjb21tb24gdHJhY2sgbmFtZXMgYXJlIG1vc3QgcG9wdWxhcj8iLA0KICAgIHN1YnRpdGxlID0gIlNpemUgb2YgcG9pbnRzIGlzIHByb3BvcnRpb25hbCB0byBmcmVxdWVuY3kgaW4gdGhlIGRhdGFzZXQuIEZyZXF1ZW5jeSBvZiBvY2N1cmFuY2VzIGluIChwYXJlbnRoZXNpcykiDQogICkNCg0KYGBgDQoNCiMjIE51bWVyaWMgRmVhdHVyZSBQbG90cw0KDQpgYGB7ciBudW1lcmljIGZlYXR1cmUgcGxvdHMsIGZpZy5hc3A9MX0NCnRyYWluX251bWVyaWMgPC0gdHJhaW5fZGYgJT4lIGtlZXAoaXMubnVtZXJpYykgJT4lIGNvbG5hbWVzKCkNCg0KdHJhaW5fZGYgJT4lDQogIHNlbGVjdF9hdChhbGxfb2YodHJhaW5fbnVtZXJpYykpICU+JQ0KICBzZWxlY3QoLWlkLCAtcG9wdWxhcml0eSkgJT4lDQogIHBpdm90X2xvbmdlcigNCiAgICBjb2xzID0gZXZlcnl0aGluZygpLA0KICAgIG5hbWVzX3RvID0gImtleSIsDQogICAgdmFsdWVzX3RvID0gInZhbHVlIg0KICApICU+JQ0KICBmaWx0ZXIoIWlzLm5hKHZhbHVlKSkgJT4lDQogIGdncGxvdChtYXBwaW5nID0gYWVzKHZhbHVlLCANCiAgICAgICAgICAgICAgICAgICAgICBhZnRlcl9zdGF0KGRlbnNpdHkpLCBmaWxsID0ga2V5LCBjb2xvciA9IGtleSkpICsNCiAgZ2VvbV9oaXN0b2dyYW0oDQogICAgcG9zaXRpb24gPSAiaWRlbnRpdHkiLA0KICAgIGJpbnMgPSAzMCwNCiAgICBzaG93LmxlZ2VuZCA9IEZBTFNFDQogICkgKw0KICBmYWNldF93cmFwKCB+IGtleSwgc2NhbGVzID0gImZyZWUiLCBuY29sID0gMykgKw0KICB0aGVtZSgNCiAgICBwbG90LnN1YnRpdGxlID0gZ2d0ZXh0OjplbGVtZW50X3RleHRib3hfc2ltcGxlKCksDQogICAgcGxvdC5iYWNrZ3JvdW5kID0gZWxlbWVudF9yZWN0KGNvbG9yID0gIndoaXRlIikNCiAgKSArDQogIGxhYnMoDQogICAgdGl0bGUgPSAiTnVtZXJpYyBGZWF0dXJlIEhpc3RvZ3JhbSBEaXN0cmlidXRpb25zIiwNCiAgICB4ID0gIk51bWVyaWMgRmVhdHVyZSIsDQogICAgeSA9IE5VTEwNCiAgKQ0KDQpgYGANCg0KQSBmYWNldCBwbG90IGZvciBhbGwgbnVtZXJpYyBmZWF0dXJlcyBvZiBwb3B1bGFyaXR5IG92ZXIgdGltZSBieSBudW1lcmljIGZlYXR1cmUuDQoNCmBgYHtyIGZhY2V0ZWQgbnVtZXJpYyBmZWF0dXJlIHdpdGggb3V0Y29tZX0NCnRyYWluX2RmICU+JSANCiAgc2VsZWN0KGlkLCBwb3B1bGFyaXR5LCBkdXJhdGlvbl9tczpyZWxlYXNlX3llYXIpICU+JSANCiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSAtYyhpZCwgcG9wdWxhcml0eSwgcmVsZWFzZV95ZWFyKSwNCiAgICAgICAgICAgICAgIG5hbWVzX3RvID0gImtleSIsDQogICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAidmFsdWUiKSAlPiUgDQogIGdncGxvdChhZXMocmVsZWFzZV95ZWFyLCB2YWx1ZSwgeiA9IHBvcHVsYXJpdHkpKSArDQogIHN0YXRfc3VtbWFyeV9oZXgoYWxwaGEgPSAwLjksIGJpbnMgPSAzMCkgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMobi5icmVha3MgPSAzKSArDQogIGZhY2V0X3dyYXAofiBrZXksIHNjYWxlcyA9ICJmcmVlIikgKw0KICBsYWJzKGZpbGwgPSAiTWVhbiBwb3B1bGFyaXR5IiwgDQogICAgICAgdGl0bGUgPSAiU3BvdGlmeSBOdW1lcmljIEZlYXR1cmVzIikgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSBjKDAuOSwgMC4yKSkNCg0KYGBgDQoNCg0KDQojIyBOdW1lcmljIEZlYXR1cmUgQ29ycmVsYXRpb25zDQoNCmBgYHtyIGNvcnJlbGF0aW9ucywgZmlnLmFzcD0xfQ0KdHJhaW5fZGYgJT4lIA0KICBzZWxlY3QoZGFuY2VhYmlsaXR5OnJlbGVhc2VfeWVhcikgJT4lIA0KICBjb3JyZWxhdGUobWV0aG9kID0gInBlYXJzb24iLA0KICAgICAgICAgICAgdXNlID0gImV2ZXJ5dGhpbmciKSAlPiUgDQogIHJlYXJyYW5nZSgpICU+JSANCiAgc2hhdmUoKSAlPiUgDQogIHJwbG90KHNoYXBlID0gMTYsIA0KICAgICAgICBwcmludF9jb3IgPSBUUlVFLCANCiAgICAgICAgbGVnZW5kID0gVFJVRSkgKw0KICBzY2FsZV94X2Rpc2NyZXRlKGd1aWRlID0gZ3VpZGVfYXhpcyhuLmRvZGdlID0gMikpDQpgYGANCg0KQWNvdXN0aWNuZXNzLCBsb3VkbmVzcywgZGFuY2VhYmlsaXR5LCBhbmQgcmVsZWFzZV95ZWFyIGFyZSBjb3JyZWxhdGVkLiBBY291c3RpY25lc3MgaXMgYW50aS1jb3JyZWxhdGVkIHdpdGggZW5lcmd5Lg0KDQojIHstfQ0KDQpDb25jbHVzaW9uOg0KDQoqIFRoZSBgZm9sbG93ZXJzYCBvZiB0aGUgYXJ0aXN0IGZlYXR1cmUgcmVxdWlyZXMgYW4gaW1wdXRhdGlvbiBmb3IgbWlzc2luZyBkYXRhIGFuZCB3b3VsZCBiZW5lZml0IGZyb20gYSBsb2cgdHJhbnNmb3JtYXRpb24uIA0KKiBXZSB3aWxsIGF0dGVtcHQgdG8gdXNlIHNvbWUgbnVtYmVyIG9mIHRoZSBtb3N0IGZyZXF1ZW50IGdlbnJlcy4gTGV0J3Mgc3RhcnQgd2l0aCA1MC4NCg0KLS0tLQ0KDQojIFByZXByb2Nlc3Npbmcgey50YWJzZXR9DQoNCiMjIFRoZSByZWNpcGUNCg0KVG8gbW92ZSBxdWlja2x5LCB0aGlzIGJhc2ljIHJlY2lwZSB0dW5lcyBub3RoaW5nLCB0YWtlcyBhbGwgb2YgdGhlIG51bWVyaWMgZmVhdHVyZXMsIGFuZCB1c2VzIG9ubHkgNTAgZ2VucmVzLg0KDQpgYGB7ciBiYXNpYyByZWNpcGV9DQpiYXNpY19yZWMgPC0NCiAgcmVjaXBlKA0KICAgIHBvcHVsYXJpdHkgfiBkdXJhdGlvbl9tcyArIGRhbmNlYWJpbGl0eSArIGVuZXJneSArIGtleSArIGxvdWRuZXNzICsgc3BlZWNoaW5lc3MgKyBhY291c3RpY25lc3MgKyBpbnN0cnVtZW50YWxuZXNzICsgbGl2ZW5lc3MgKyB2YWxlbmNlICsgdGVtcG8gKyByZWxlYXNlX3llYXIgKyBmb2xsb3dlcnMgKyBnZW5yZXMsDQogICAgZGF0YSA9IHRyYWluX2RmDQogICkgJT4lDQogIHN0ZXBfdG9rZW5pemUoZ2VucmVzKSAlPiUNCiAgc3RlcF90b2tlbmZpbHRlcihnZW5yZXMsIG1heF90b2tlbnMgPSA1MCkgJT4lDQogIHN0ZXBfdGYoZ2VucmVzLCB3ZWlnaHRfc2NoZW1lID0gImJpbmFyeSIpICU+JQ0KICBzdGVwX211dGF0ZV9hdChjb250YWlucygidGZfZ2VucmVzIiksDQogICAgICAgICAgICAgIGZuID0gfiBpZl9lbHNlKC4gPT0gVFJVRSwgMSwgMCkpICU+JQ0KICBzdGVwX2ltcHV0ZV9tZWRpYW4oZm9sbG93ZXJzKSAlPiUNCiAgc3RlcF9sb2coZm9sbG93ZXJzLCBkdXJhdGlvbl9tcywgb2Zmc2V0ID0gIDEpDQoNCmBgYA0KDQojIyBkYXRhc2V0IGZvciBtb2RlbGluZw0KDQpgYGB7ciBqdWljZSB0aGUgZGF0YXNldH0NCmJhc2ljX3JlYyAlPiUgDQojICBmaW5hbGl6ZV9yZWNpcGUobGlzdChudW1fY29tcCA9IDIpKSAlPiUgDQogIHByZXAoKSAlPiUgDQogIGp1aWNlKCkgDQoNCmBgYA0KDQojIyBDcm9zcyBWYWxpZGF0aW9uDQoNCldlIHdpbGwgdXNlIDUtZm9sZCBjcm9zcyB2YWxpZGF0aW9uIGFuZCBzdHJhdGlmeSBvbiBwb3B1bGFyaXR5IHRvIGJ1aWxkIG1vZGVscyB0aGF0IGFyZSBsZXNzIGxpa2VseSB0byBvdmVyLWZpdCB0aGUgdHJhaW5pbmcgZGF0YS4NCg0KUHJvcGVyIGJ1c2luZXNzIG1vZGVsaW5nIHByYWN0aWNlIHdvdWxkIGhvbGRvdXQgYSBzYW1wbGUgZnJvbSB0cmFpbmluZyBlbnRpcmVseSBmb3IgYXNzZXNzaW5nIG1vZGVsIHBlcmZvcm1hbmNlLiBPbiB0aGUgS2FnZ2xlIGNvbXBldGl0aW9ucywgdGhlIGhvbGRvdXQgaXMgdGhlICJ0ZXN0IiBpdHNlbGYgaW4gdGhlIGZvcm0gb2YgdGhlIHRoZSBjb21wZXRpdG9yIHN1Ym1pc3Npb24uDQoNCmBgYHtyIGNyb3NzIHZhbGlkYXRpb259DQpzZXQuc2VlZCgyMDIxKQ0KDQooZm9sZHMgPC0gdmZvbGRfY3YodHJhaW5fZGYsIHYgPSA1LCBzdHJhdGEgPSBwb3B1bGFyaXR5KSkNCg0KYGBgDQoNCiMgey19DQoNCiMgTWFjaGluZSBMZWFybmluZzogUmFuZG9tIEZvcmVzdCB7LnRhYnNldH0NCg0KTGV0J3MgcnVuIG1vZGVscyBpbiB0d28gc3RlcHMuIFRoZSBmaXJzdCBpcyBhIHNpbXBsZSwgZmFzdCBzaGFsbG93IHJhbmRvbSBmb3Jlc3QsIHRvIGNvbmZpcm0gdGhhdCB0aGUgbW9kZWwgd2lsbCBydW4gYW5kIG9ic2VydmUgZmVhdHVyZSBpbXBvcnRhbmNlIHNjb3Jlcy4gVGhlIHNlY29uZCB3aWxsIHVzZSBgeGdib29zdGAuIEJvdGggdXNlIHRoZSBiYXNpYyByZWNpcGUgcHJlcHJvY2Vzc29yIGZvciBub3cuDQoNCiMjIE1vZGVsIFNwZWNpZmljYXRpb24NCg0KVGhpcyBmaXJzdCBtb2RlbCBpcyBhIGJhZ2dlZCB0cmVlLCB3aGVyZSB0aGUgbnVtYmVyIG9mIHByZWRpY3RvcnMgdG8gY29uc2lkZXIgZm9yIGVhY2ggc3BsaXQgb2YgYSB0cmVlIChpLmUuLCBtdHJ5KSBlcXVhbHMgdGhlIG51bWJlciBvZiBhbGwgYXZhaWxhYmxlIHByZWRpY3RvcnMuIFRoZSBgbWluX25gIG9mIDEwIG1lYW5zIHRoYXQgZWFjaCB0cmVlIGJyYW5jaCBvZiB0aGUgNTAgZGVjaXNpb24gdHJlZXMgYnVpbHQgaGF2ZSBhdCBsZWFzdCAxMCBvYnNlcnZhdGlvbnMuIEFzIGEgcmVzdWx0LCB0aGUgZGVjaXNpb24gdHJlZXMgaW4gdGhlIGVuc2VtYmxlIGFsbCBhcmUgcmVsYXRpdmVseSBzaGFsbG93Lg0KDQpgYGB7ciByYW5kb20gZm9yZXN0IHNwZWN9DQoNCihiYWdfc3BlYyA8LQ0KICBiYWdfdHJlZShtaW5fbiA9IDEwKSAlPiUNCiAgc2V0X2VuZ2luZSgicnBhcnQiLCB0aW1lcyA9IDUwKSAlPiUNCiAgc2V0X21vZGUoInJlZ3Jlc3Npb24iKSkNCg0KYGBgDQoNCiMjIFBhcmFsbGVsIGJhY2tlbmQNCg0KVG8gc3BlZWQgdXAgY29tcHV0YXRpb24gd2Ugd2lsbCB1c2UgYSBwYXJhbGxlbCBiYWNrZW5kLg0KDQpgYGB7ciBwYXJhbGxlbCBiYWNrZW5kfQ0KYWxsX2NvcmVzIDwtIHBhcmFsbGVsbHk6OmF2YWlsYWJsZUNvcmVzKG9taXQgPSAxKQ0KYWxsX2NvcmVzDQoNCmZ1dHVyZTo6cGxhbigibXVsdGlzZXNzaW9uIiwgd29ya2VycyA9IGFsbF9jb3JlcykgIyBvbiBXaW5kb3dzDQoNCmBgYA0KDQojIyBGaXQgYW5kIFZhcmlhYmxlIEltcG9ydGFuY2UNCg0KTGV0cyBtYWtlIGEgY3Vyc29yeSBjaGVjayBvZiB0aGUgcmVjaXBlIGFuZCB2YXJpYWJsZSBpbXBvcnRhbmNlLCB3aGljaCBjb21lcyBvdXQgb2YgYHJwYXJ0YCBmb3IgZnJlZS4gVGhpcyB3b3JrZmxvdyBhbHNvIGhhbmRsZXMgZmFjdG9ycyB3aXRob3V0IGR1bW1pZXMuDQoNCmBgYHtyIGZpdCByYW5kb20gZm9yZXN0fQ0KYmFnX3dmIDwtDQogIHdvcmtmbG93KCkgJT4lDQogIGFkZF9yZWNpcGUoYmFzaWNfcmVjKSAlPiUNCiAgYWRkX21vZGVsKGJhZ19zcGVjKQ0KDQpiYWdfZml0IDwtIHBhcnNuaXA6OmZpdChiYWdfd2YsIGRhdGEgPSB0cmFpbl9kZikNCg0KZXh0cmFjdF9maXRfcGFyc25pcChiYWdfZml0KSRmaXQkaW1wICU+JQ0KICBtdXRhdGUodGVybSA9IGZjdF9yZW9yZGVyKHRlcm0sIHZhbHVlKSkgJT4lDQogIGdncGxvdChhZXModmFsdWUsIHRlcm0pKSArDQogIGdlb21fcG9pbnQoKSArDQogIGdlb21fZXJyb3JiYXJoKGFlcygNCiAgICB4bWluID0gdmFsdWUgLSBgc3RkLmVycm9yYCAvIDIsDQogICAgeG1heCA9IHZhbHVlICsgYHN0ZC5lcnJvcmAgLyAyDQogICksDQogIGhlaWdodCA9IC4zKSArDQogIGxhYnModGl0bGUgPSAiRmVhdHVyZSBJbXBvcnRhbmNlIiwNCiAgICAgICB4ID0gTlVMTCwgeSA9IE5VTEwpDQoNCmBgYA0KDQoNCmBgYHtyIHBlcmZvcm1hbmNlIG9mIHJhbmRvbSBmb3Jlc3R9DQphdWdtZW50KGJhZ19maXQsIHRyYWluX2RmKSAlPiUgDQogIHNlbGVjdChpZCwgcG9wdWxhcml0eSwgLnByZWQpICU+JSANCiAgcm1zZSh0cnV0aCA9IHBvcHVsYXJpdHksIGVzdGltYXRlID0gLnByZWQpDQpgYGANCg0KVGhlIFJNU0UgaGVyZSBpcyBub3QgYSBncmVhdCByZXN1bHQuIEl0IHN1Z2dlc3RzIHRoYXQgZm9yIHRoaXMgbW9kZWwsIHNvbmdzIHdoZXJlIHRoZSBwb3B1bGFyaXR5IGlzIG5lYXIgdGhlIG1lYW4gb2YgMjcsIHRoZSBwcmVkaWN0aW9uIHlpZWxkcyBhbiBlcnJvciBvZiBhcm91bmQgNS44IG9uIHRoZSB0cmFpbmluZyBkYXRhLiBFdmVuIHdvcnNlLCB0aGUgbW9kZWwgaXMgbGlrZWx5IG92ZXItZml0IGFuZCB3b3VsZCBwZXJmb3JtIHdvcnNlIG9uIGhvbGRvdXQgZGF0YS4NCg0KRXZlbiBzbywgbGV0J3MgYmFuayB0aGlzIGZpcnN0IHN1Ym1pc3Npb24gdG8gS2FnZ2xlIGFzLWlzLCBhbmQgd29yayBtb3JlIHdpdGggYHhnYm9vc3RgIHRvIGRvIGJldHRlci4NCg0KYGBge3Igd3JpdGUgY3N2IHJhbmRvbSBmb3Jlc3QsIGV2YWwgPSBGQUxTRX0NCmF1Z21lbnQoYmFnX2ZpdCwgdGVzdF9kZikgJT4lIA0KICBzZWxlY3QoaWQsIHBvcHVsYXJpdHkgPSAucHJlZCkgJT4lIA0KICB3cml0ZV9jc3YoZmlsZSA9IHBhdGhfZXhwb3J0KQ0KYGBgDQoNCmBgYHtyIHBvc3QgY3N2IHJhbmRvbSBmb3Jlc3QsIGV2YWwgPSBGQUxTRX0NCnNoZWxsKGdsdWU6OmdsdWUoJ2thZ2dsZSBjb21wZXRpdGlvbnMgc3VibWl0IC1jIHsgY29tcGV0aXRpb25fbmFtZSB9IC1mIHsgcGF0aF9leHBvcnQgfSAtbSAiRmlyc3QgbW9kZWwiJykpDQpgYGANCg0KIyB7LX0NCg0KIyBNYWNoaW5lIExlYXJuaW5nOiBYR0Jvb3N0IE1vZGVsIDEgey50YWJzZXR9DQoNCiMjIE1vZGVsIFNwZWNpZmljYXRpb24NCg0KTGV0J3Mgc3RhcnQgd2l0aCBhIGJvb3N0ZWQgbW9kZWwgdGhhdCBydW5zIGZhc3QgYW5kIGdpdmVzIGFuIGVhcmx5IGluZGljYXRpb24gb2Ygd2hpY2ggaHlwZXJwYXJhbWV0ZXJzIG1ha2UgdGhlIG1vc3QgZGlmZmVyZW5jZSBpbiBtb2RlbCBwZXJmb3JtYW5jZS4NCg0KYGBge3IgeGdib29zdCBzcGVjIG9uZX0NCih4Z2Jvb3N0X3NwZWMgPC0gYm9vc3RfdHJlZSh0cmVlcyA9IHR1bmUoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fbiA9IHR1bmUoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZWFybl9yYXRlID0gdHVuZSgpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyZWVfZGVwdGggPSB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RvcF9pdGVyID0gMjApICU+JSANCiAgc2V0X2VuZ2luZSgieGdib29zdCIsIHZhbGlkYXRpb24gPSAwLjIpICU+JQ0KICBzZXRfbW9kZSgicmVncmVzc2lvbiIpKQ0KYGBgDQoNCiMjIFR1bmluZyBhbmQgUGVyZm9ybWFuY2UNCg0KVGhlIGdyaWQgaGVyZSBpcyBvbmx5IDcgY29tYmluYXRpb25zIG9mIHRoZSBkZWZhdWx0IHBhcmFtZXRlcnMgYW5kIHRoZSBgdHVuZV9ncmlkYCBwcm9jZXNzIGlzIHNldHVwIHRvIHN0b3AgZWFybHkuDQoNCmBgYHtyIHR1bmUgZ3JpZCB4Z2Jvb3N0IG9uZX0NCnNldC5zZWVkKDIwMjEpDQpjdl9yZXNfeGdib29zdCA8LQ0KICB3b3JrZmxvdygpICU+JSANCiAgYWRkX3JlY2lwZShiYXNpY19yZWMpICU+JSANCiAgYWRkX21vZGVsKHhnYm9vc3Rfc3BlYykgJT4lIA0KICB0dW5lX2dyaWQoICAgIA0KICAgIHJlc2FtcGxlcyA9IGZvbGRzLA0KICAgIGdyaWQgPSA3LA0KICAgIG1ldHJpY3MgPSBtc2V0DQopDQpgYGANCg0KYGBge3Igb25lIHBlcmZvcm1hbmNlfQ0KYXV0b3Bsb3QoY3ZfcmVzX3hnYm9vc3QpDQoNCmNvbGxlY3RfbWV0cmljcyhjdl9yZXNfeGdib29zdCkgJT4lIA0KICBhcnJhbmdlKG1lYW4pDQpgYGANCg0KVGhlIGJlc3QgbGVhcm5pbmcgcmF0ZXMgYXJlIHZlcnkgc21hbGwsIGFyb3VuZCAwLjAxMy4gU2V0dGluZyB0cmVlcyBhdCA3NTMgYW5kIGRlcHRoIGF0IDIgYXBwZWFycyB0byBnaXZlIHN0YWJsZSByZXN1bHRzLg0KDQojIHstfQ0KDQojIE1hY2hpbmUgTGVhcm5pbmc6IFhHQm9vc3QgTW9kZWwgMiB7LnRhYnNldH0NCg0KTGV0J3MgdXNlIHdoYXQgd2UgbGVhcm5lZCBhYm92ZSB0byBzZXQgdGhlIGxlYXJuaW5nIHJhdGUsIHRoZSBudW1iZXIgb2YgdHJlZXMgYW5kIHRyZWUgZGVwdGgsIGFuZCBhZGQgYmFjayBvdGhlciBoeXBlcnBhcmFtZXRlcnMgZm9yIGZ1cnRoZXIgdHVuaW5nLiBUaGlzIHRpbWUsIGxldCdzIGFsc28gdHJ5IHRoZWB0dW5lX3JhY2VfYW5vdmFgIHRlY2huaXF1ZSBmb3Igc2tpcHBpbmcgdGhlIHBhcnRzIG9mIHRoZSBncmlkIHNlYXJjaCB0aGF0IGRvIG5vdCBwZXJmb3JtIHdlbGwuDQoNCiMjIE1vZGVsIFNwZWNpZmljYXRpb24NCg0KYGBge3Igc3BlYyB4Z2Jvb3N0IHR3b30NCih4Z2Jvb3N0X3NwZWMgPC0gYm9vc3RfdHJlZSh0cmVlcyA9IDc1MywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fbiA9IHR1bmUoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZWFybl9yYXRlID0gMC4wMTMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJlZV9kZXB0aCA9IDIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgc2FtcGxlX3NpemUgPSB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgbG9zc19yZWR1Y3Rpb24gPSB0dW5lKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RvcF9pdGVyID0gMjApICU+JSANCiAgc2V0X2VuZ2luZSgieGdib29zdCIsIHZhbGlkYXRpb24gPSAwLjEpICU+JQ0KICBzZXRfbW9kZSgicmVncmVzc2lvbiIpKQ0KDQp4Z2Jvb3N0X3BhcmFtIDwtIHBhcmFtZXRlcnMoeGdib29zdF9zcGVjKSAlPiUNCiAgdXBkYXRlKA0KICAgIG1pbl9uID0gbWluX24oYyg0LCAzOSkpDQogICkNCmBgYA0KDQojIyBUdW5pbmcgYW5kIFBlcmZvcm1hbmNlDQoNCmBgYHtyIHR1bmUgZ3JpZCB4Z2Jvb3N0IHR3byBub2V2YWwsIGV2YWwgPSBGQUxTRX0NCmN2X3Jlc194Z2Jvb3N0IDwtDQogIHdvcmtmbG93KCkgJT4lIA0KICBhZGRfcmVjaXBlKGJhc2ljX3JlYykgJT4lIA0KICBhZGRfbW9kZWwoeGdib29zdF9zcGVjKSAlPiUgDQogIHR1bmVfcmFjZV9hbm92YSggICAgDQogICAgcmVzYW1wbGVzID0gZm9sZHMsDQogICAgZ3JpZCA9IHhnYm9vc3RfcGFyYW0gJT4lIGdyaWRfbWF4X2VudHJvcHkoc2l6ZSA9IDEyKSwNCiAgICBjb250cm9sID0gY29udHJvbF9yYWNlKHZlcmJvc2UgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVfcHJlZCA9IFRSVUUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV93b3JrZmxvdyA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0ID0gZXh0cmFjdF9tb2RlbCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFsbGVsX292ZXIgPSAicmVzYW1wbGVzIiksDQogICAgbWV0cmljcyA9IG1zZXQNCikNCmBgYA0KDQoNCmBgYHtyIHR1bmUgZ3JpZCB4Z2Jvb3N0IHR3byBub2luY2x1ZGUsIGluY2x1ZGU9RkFMU0V9DQppZiAoZmlsZS5leGlzdHMoaGVyZTo6aGVyZSgiZGF0YSIsICJzcG90aWZ5eGdib29zdC5yZHMiKSkpIHsNCmN2X3Jlc194Z2Jvb3N0IDwtIHJlYWRfcmRzKGhlcmU6OmhlcmUoImRhdGEiLCAic3BvdGlmeXhnYm9vc3QucmRzIikpICANCn0gZWxzZSB7DQpjdl9yZXNfeGdib29zdCA8LQ0KICB3b3JrZmxvdygpICU+JSANCiAgYWRkX3JlY2lwZShiYXNpY19yZWMpICU+JSANCiAgYWRkX21vZGVsKHhnYm9vc3Rfc3BlYykgJT4lIA0KICB0dW5lX3JhY2VfYW5vdmEoICAgIA0KICAgIHJlc2FtcGxlcyA9IGZvbGRzLA0KICAgIGdyaWQgPSB4Z2Jvb3N0X3BhcmFtICU+JSBncmlkX21heF9lbnRyb3B5KHNpemUgPSAxMiksDQogICAgY29udHJvbCA9IGNvbnRyb2xfcmFjZSh2ZXJib3NlID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzYXZlX3ByZWQgPSBUUlVFLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVfd29ya2Zsb3cgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZXh0cmFjdCA9IGV4dHJhY3RfbW9kZWwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbGxlbF9vdmVyID0gInJlc2FtcGxlcyIpLA0KICAgIG1ldHJpY3MgPSBtc2V0DQopDQp3cml0ZV9yZHMoY3ZfcmVzX3hnYm9vc3QsIGhlcmU6OmhlcmUoImRhdGEiLCAic3BvdGlmeXhnYm9vc3QucmRzIikpDQp9DQoNCg0KYGBgDQoNCg0KYGBge3IgcGVyZm9ybWFuY2UgeGdib29zdCB0d299DQphdXRvcGxvdChjdl9yZXNfeGdib29zdCkNCg0KY29sbGVjdF9tZXRyaWNzKGN2X3Jlc194Z2Jvb3N0KSAlPiUgDQogIGFycmFuZ2UobWVhbikNCg0KYGBgDQoNClRoZSBiZXN0IG1lYW4gcm1zZSBhY3Jvc3MgZm9sZHMgaXMgYWJvdXQgMTIsIHdoaWNoIGlzIGRpc2NvdXJhZ2luZy4gVGhpcyBmaWd1cmUgaXMgbGlrZWx5IG1vcmUgcm9idXN0IGFuZCBhIGJldHRlciBlc3RpbWF0ZSBvZiBwZXJmb3JtYW5jZSBvbiBob2xkb3V0IGRhdGEuIExldCdzIGZpdCBvbiB0aGUgZW50aXJlIHRyYWluaW5nIHNldCBhdCB0aGVzZSBoeXBlcnBhcmFtZXRlcnMgdG8gZ2V0IGEgc2luZ2xlIFJNU0UgcGVyZm9ybWFuY2UgZXN0aW1hdGUgb24gdGhlIGJlc3QgbW9kZWwgc28gZmFyLg0KDQpgYGB7ciBmaXQgeGdib29zdCB0d28gYW5kIHBlcmZvcm1hbmNlfQ0KeGdiX3dmX2Jlc3QgPC0gICANCiAgd29ya2Zsb3coKSAlPiUgDQogIGFkZF9yZWNpcGUoYmFzaWNfcmVjKSAlPiUgDQogIGFkZF9tb2RlbCh4Z2Jvb3N0X3NwZWMpICU+JSANCiAgZmluYWxpemVfd29ya2Zsb3coc2VsZWN0X2Jlc3QoY3ZfcmVzX3hnYm9vc3QpKQ0KDQpmaXRfYmVzdCA8LSB4Z2Jfd2ZfYmVzdCAlPiUNCiAgcGFyc25pcDo6Zml0KGRhdGEgPSB0cmFpbl9kZikNCg0KYXVnbWVudChmaXRfYmVzdCwgdHJhaW5fZGYpICU+JSANCiAgc2VsZWN0KGlkLCBwb3B1bGFyaXR5LCAucHJlZCkgJT4lIA0KICBybXNlKHRydXRoID0gcG9wdWxhcml0eSwgZXN0aW1hdGUgPSAucHJlZCkNCmBgYA0KDQpMZXQncyBiYW5rIHRoaXMgc2Vjb25kIHN1Ym1pc3Npb24gdG8gS2FnZ2xlLCBhbmQgd29yayBldmVuIG1vcmUgd2l0aCBgeGdib29zdGAgdG8gZG8gYmV0dGVyLg0KDQpgYGB7ciB3cml0ZSB4Z2Jvb3N0IHR3bywgZXZhbD1GQUxTRX0NCmF1Z21lbnQoZml0X2Jlc3QsIHRlc3RfZGYpICU+JSANCiAgc2VsZWN0KGlkLCBwb3B1bGFyaXR5ID0gLnByZWQpICU+JSANCiAgd3JpdGVfY3N2KGZpbGUgPSBwYXRoX2V4cG9ydCkNCmBgYA0KDQpgYGB7ciBzdWJtaXQgeGdib29zdCB0d28sIGV2YWwgPSBGQUxTRX0NCnNoZWxsKGdsdWU6OmdsdWUoJ2thZ2dsZSBjb21wZXRpdGlvbnMgc3VibWl0IC1jIHsgY29tcGV0aXRpb25fbmFtZSB9IC1mIHsgcGF0aF9leHBvcnQgfSAtbSAiU2Vjb25kIG1vZGVsIicpKQ0KYGBgDQoNCiMjIFNIQVBsZXkgVmFyaWFibGUgSW1wb3J0YW5jZQ0KDQpMZXQncyB0YWtlIGEgZGVlcGVyIGRpdmUgaW50byB0aGUgWEdCb29zdCB2YXJpYWJsZSBpbXBvcnRhbmNlLg0KDQpgYGB7ciB2YXJpYWJsZSBpbXBvcnRhbmNlIHhnYm9vc3QgdHdvfQ0KZml0X2Jlc3QgJT4lIA0KICBleHRyYWN0X2ZpdF9wYXJzbmlwKCkgJT4lIA0KICB2aXAoZ2VvbSA9ICJwb2ludCIpICsNCiAgbGFicyh0aXRsZSA9ICJYR0Jvb3N0IG1vZGVsIFZhcmlhYmxlIEltcG9ydGFuY2UiLA0KICAgICAgIHN1YnRpdGxlID0gIlZJUCBwYWNrYWdlIikNCg0KYGBgDQoNCkZvciBpbmZlcmVuY2UsIHRoZSBYR0Jvb3N0IG1vZGVsIHN1Z2dlc3RzIHRoYXQgYWZ0ZXIgYHJlbGVhc2VfeWVhcmAgYW5kIGBmb2xsb3dlcnNgLCB0aGUgdW5kZXJseWluZyBmZWF0dXJlcyB0aGF0IGRyaXZlIHRoZSBwb3B1bGFyaXR5IGFyZSBzb21ld2hhdCBkaWZmZXJlbnQuICBMZXQncyBsb29rIGNsb3NlciB0byB1bmRlcnN0YW5kIHdoYXQgaXMgZ29pbmcgb24gdG8gaW1wcm92ZSB0aGUgbW9kZWwgZnVydGhlci4NCg0KYGBge3Igc2hhcGxleSB4Z2Jvb3N0IHR3bywgZmlnLmFzcD0xfQ0Kc29tZV90cmFpbmluZ19kYXRhIDwtIGJhc2ljX3JlYyAlPiUgcHJlcCgpICU+JSBiYWtlKG5ld19kYXRhID0gc2xpY2Vfc2FtcGxlKHRyYWluX2RmLCBuID0gNTAwKSAsIGNvbXBvc2l0aW9uID0gIm1hdHJpeCIpIA0KDQpwYXJzbmlwX2ZpdCA8LSAgZXh0cmFjdF9maXRfcGFyc25pcChmaXRfYmVzdCkNCg0KIyByZW1vdmUgdGhlIG91dGNvbWUgdmFyaWFibGUNCnNoYXAgPC0gc2hhcC5wcmVwKHBhcnNuaXBfZml0JGZpdCwgWF90cmFpbiA9IHNvbWVfdHJhaW5pbmdfZGF0YVssLTE0XSkNCg0KIyBzaGFwLmltcG9ydGFuY2Uoc2hhcCwgbmFtZXNfb25seSA9IFRSVUUpDQoNCnNoYXAucGxvdC5zdW1tYXJ5KHNoYXApDQoNCmBgYA0KDQpBcyBtZW50aW9uZWQgYmVmb3JlLCBgcmVsZWFzZV95ZWFyYCBhbmQgdGhlIGFydGlzdCdzIGBmb2xsb3dlcnNgIChsb2cgc2NhbGUpIGhhdmUgYSBsYXJnZSBpbmZsdWVuY2Ugb24gcG9wdWxhcml0eSBpbiB0aGUgbW9kZWwgb3V0cHV0LiBMZXQncyBsb29rIGF0IGluZGl2aWR1YWwgZmVhdHVyZXMuDQoNCmBgYHtyIHhnYm9vc3QgdHdvIHJlbGVhc2UgeWVhciBwZH0NCnNoYXAucGxvdC5kZXBlbmRlbmNlKHNoYXAsICJyZWxlYXNlX3llYXIiLA0KICAgICAgICAgICAgICAgICAgICAgY29sb3JfZmVhdHVyZSA9ICJhdXRvIiwgYWxwaGEgPSAwLjYsDQogICAgICAgICAgICAgICAgICAgICBqaXR0ZXJfd2lkdGggPSAwLjEpDQpgYGANCg0KVGhlIHJlbGVhc2UgeWVhciBTSEFQIHZhbHVlcyBhcmUgYWxtb3N0IGxpbmVhci4gVGhlcmUgbWF5IGJlIGFuIGludGVyYWN0aW9uIHdpdGggYGVuZXJneWAgdGhhdCB3b3VsZCBpbXByb3ZlIHRoZSBtb2RlbC4NCg0KYGBge3IgeGdib29zdCB0d28gZm9sbG93ZXJzIHBkfQ0Kc2hhcC5wbG90LmRlcGVuZGVuY2Uoc2hhcCwgImZvbGxvd2VycyIsDQogICAgICAgICAgICAgICAgICAgICBjb2xvcl9mZWF0dXJlID0gImF1dG8iLCBhbHBoYSA9IDAuNiwNCiAgICAgICAgICAgICAgICAgICAgIGppdHRlcl93aWR0aCA9IDAuMSkNCmBgYA0KDQpUaGUgbG9nIHRyYW5zZm9ybWF0aW9uIG9mIGZvbGxvd2VycyB3YXMganVzdGlmaWVkLCB0aG91Z2ggc29tZXRoaW5nIHN0cmFuZ2UgbXVzdCBiZSBoYXBwZW5pbmcgYXQgMTIuIFRoZXJlIG1heSBiZSBhbiBpbnRlcmFjdGlvbiB3aXRoIGByZWxlYXNlX3llYXJgIHRoYXQgY291bGQgaW1wcm92ZSB0aGUgbW9kZWwuDQoNCkFuZCBmb3IgdGhlIGZlYXR1cmUgYGluc3RydW1lbnRhbG5lc3NgOg0KDQpgYGB7ciB4Z2Jvb3N0IHR3byBpbnN0cnVtZW50YWxuZXNzIHBkfQ0Kc2hhcC5wbG90LmRlcGVuZGVuY2Uoc2hhcCwgImluc3RydW1lbnRhbG5lc3MiLA0KICAgICAgICAgICAgICAgICAgICAgY29sb3JfZmVhdHVyZSA9ICJhdXRvIiwgYWxwaGEgPSAwLjYsDQogICAgICAgICAgICAgICAgICAgICBqaXR0ZXJfd2lkdGggPSAwLjAyKQ0KYGBgDQoNCiMjIERBTEVYIFBhcnRpYWwgRGVwZW5kZW5jZSBQbG90cw0KDQpXaGF0IGlzIHRoZSBhZ2dyZWdhdGVkIGVmZmVjdCBvZiB0aGUgYHJlbGVhc2VfeWVhcmAgZmVhdHVyZSBvdmVyIDUwMCBleGFtcGxlcz8NCg0KYGBge3IgeGdib29zdCB0d28gREFMRVggcmVsZWFzZV95ZWFyfQ0KZXhwbGFpbmVyX3hnYiA8LSBleHBsYWluX3RpZHltb2RlbHMoDQogIGZpdF9iZXN0LA0KICB0cmFpbl9kZiAlPiUgc2VsZWN0KC1wb3B1bGFyaXR5KSwNCiAgdHJhaW5fZGYkcG9wdWxhcml0eQ0KKQ0KDQpwZHBfeWVhciA8LSBtb2RlbF9wcm9maWxlKGV4cGxhaW5lcl94Z2IsDQogICAgICAgICAgICAgIE4gPSA1MDAsDQogICAgICAgICAgICAgIHZhcmlhYmxlcyA9ICJyZWxlYXNlX3llYXIiKQ0KDQphc190aWJibGUocGRwX3llYXIkYWdyX3Byb2ZpbGVzKSAlPiUgDQogIGdncGxvdChhZXMoYF94X2AsIGBfeWhhdF9gKSkgKw0KICBnZW9tX2xpbmUoZGF0YSA9IGFzX3RpYmJsZSgNCiAgICAgcGRwX3llYXIkY3BfcHJvZmlsZXMpLA0KICAgICBhZXMocmVsZWFzZV95ZWFyLCBncm91cCA9IGBfaWRzX2ApLA0KICAgICBzaXplID0gMC41LCBhbHBoYSA9IDAuMSwgY29sb3IgPSAiZ3JheTMwIg0KICAgKSArDQogIGdlb21fbGluZShzaXplID0gMS4yLCBhbHBoYSA9IDAuOCwgY29sb3IgPSAib3JhbmdlIikgKw0KICBsYWJzKHggPSAiWWVhciIsIHkgPSAiUHJlZGljdGVkIFBvcHVsYXJpdHkiKQ0KICANCmBgYA0KDQpXaGF0IGlzIHRoZSBhZ2dyZWdhdGVkIGVmZmVjdCBvZiB0aGUgYGZvbGxvd2Vyc2AgZmVhdHVyZSBvdmVyIDUwMCBleGFtcGxlcz8NCg0KYGBge3IgeGdib29zdCB0d28gREFMRVggZm9sbG93ZXJzfQ0KcGRwX2ZvbGxvd2VycyA8LSBtb2RlbF9wcm9maWxlKGV4cGxhaW5lcl94Z2IsDQogICAgICAgICAgICAgIE4gPSA1MDAsDQogICAgICAgICAgICAgIHZhcmlhYmxlcyA9ICJmb2xsb3dlcnMiKQ0KDQphc190aWJibGUocGRwX2ZvbGxvd2VycyRhZ3JfcHJvZmlsZXMpICU+JSANCiAgZ2dwbG90KGFlcyhgX3hfYCwgYF95aGF0X2ApKSArDQogIGdlb21fbGluZShkYXRhID0gYXNfdGliYmxlKA0KICAgICBwZHBfZm9sbG93ZXJzJGNwX3Byb2ZpbGVzKSwNCiAgICAgYWVzKGZvbGxvd2VycywgZ3JvdXAgPSBgX2lkc19gKSwNCiAgICAgc2l6ZSA9IDAuNSwgYWxwaGEgPSAwLjEsIGNvbG9yID0gImdyYXkzMCINCiAgICkgKw0KICBnZW9tX2xpbmUoc2l6ZSA9IDEuMiwgYWxwaGEgPSAwLjgsIGNvbG9yID0gImRhcmtibHVlIikgKw0KICBsYWJzKHggPSAiQXJ0aXN0IEZvbGxvd2VycyIsIHkgPSAiUHJlZGljdGVkIFBvcHVsYXJpdHkiKQ0KICANCmBgYA0KDQpXaGF0IGlzIHRoZSBhZ2dyZWdhdGVkIGVmZmVjdCBvZiB0aGUgYGFjb3VzdGljbmVzc2AgZmVhdHVyZSBvdmVyIDUwMCBleGFtcGxlcz8NCg0KYGBge3IgeGdib29zdCB0d28gREFMRVggYWNvdXN0aWNuZXNzfQ0KcGRwX2Fjb3VzdGljbmVzcyA8LSBtb2RlbF9wcm9maWxlKGV4cGxhaW5lcl94Z2IsDQogICAgICAgICAgICAgIE4gPSA1MDAsDQogICAgICAgICAgICAgIHZhcmlhYmxlcyA9ICJhY291c3RpY25lc3MiKQ0KDQphc190aWJibGUocGRwX2Fjb3VzdGljbmVzcyRhZ3JfcHJvZmlsZXMpICU+JSANCiAgZ2dwbG90KGFlcyhgX3hfYCwgYF95aGF0X2ApKSArDQogIGdlb21fbGluZShkYXRhID0gYXNfdGliYmxlKA0KICAgICBwZHBfYWNvdXN0aWNuZXNzJGNwX3Byb2ZpbGVzKSwNCiAgICAgYWVzKGFjb3VzdGljbmVzcywgZ3JvdXAgPSBgX2lkc19gKSwNCiAgICAgc2l6ZSA9IDAuNSwgYWxwaGEgPSAwLjEsIGNvbG9yID0gImdyYXkzMCINCiAgICkgKw0KICBnZW9tX2xpbmUoc2l6ZSA9IDEuMiwgYWxwaGEgPSAwLjgsIGNvbG9yID0gImdyZWVuIikgKw0KICBsYWJzKHggPSAiQXJ0aXN0IGFjb3VzdGljbmVzcyIsIHkgPSAiUHJlZGljdGVkIFBvcHVsYXJpdHkiKQ0KICANCmBgYA0KDQpXaGF0IGlzIHRoZSBhZ2dyZWdhdGVkIGVmZmVjdCBvZiB0aGUgYGR1cmF0aW9uYCBmZWF0dXJlIG92ZXIgNTAwIGV4YW1wbGVzPw0KDQpgYGB7ciB4Z2Jvb3N0IHR3byBEQUxFWCBkdXJhdGlvbn0NCnBkcF9kdXJhdGlvbiA8LSBtb2RlbF9wcm9maWxlKGV4cGxhaW5lcl94Z2IsDQogICAgICAgICAgICAgIE4gPSA1MDAsDQogICAgICAgICAgICAgIHZhcmlhYmxlcyA9ICJkdXJhdGlvbl9tcyIpDQoNCmFzX3RpYmJsZShwZHBfZHVyYXRpb24kYWdyX3Byb2ZpbGVzKSAlPiUgDQogIGdncGxvdChhZXMoYF94X2AsIGBfeWhhdF9gKSkgKw0KICBnZW9tX2xpbmUoZGF0YSA9IGFzX3RpYmJsZSgNCiAgICAgcGRwX2R1cmF0aW9uJGNwX3Byb2ZpbGVzKSwNCiAgICAgYWVzKGR1cmF0aW9uX21zLCBncm91cCA9IGBfaWRzX2ApLA0KICAgICBzaXplID0gMC41LCBhbHBoYSA9IDAuMSwgY29sb3IgPSAiZ3JheTMwIg0KICAgKSArDQogIGdlb21fbGluZShzaXplID0gMS4yLCBhbHBoYSA9IDAuOCwgY29sb3IgPSAiZGFya2dyZWVuIikgKw0KICBsYWJzKHggPSAiU29uZyBEdXJhdGlvbiAobXMpIiwgeSA9ICJQcmVkaWN0ZWQgUG9wdWxhcml0eSIpDQogIA0KYGBgDQoNCiMgey19DQoNCiMgTWFjaGluZSBMZWFybmluZyBYR0Jvb3N0IDMgey50YWJzZXR9DQoNCkxldCdzIGFkZCBvdXIgaW50ZXJhY3Rpb25zIGZyb20gYWJvdmUgYW5kIGludHJvZHVjZSBkaW1lbnNpb25hbGl0eSByZWR1Y3Rpb24uDQoNCiMjIFJlY2lwZQ0KDQpQcmluY2lwYWwgY29tcG9uZW50IGFuYWx5c2lzIChQQ0EpIGlzIGEgdHJhbnNmb3JtYXRpb24gb2YgYSBncm91cCBvZiB2YXJpYWJsZXMgdGhhdCBwcm9kdWNlcyBhIG5ldyBzZXQgb2YgYXJ0aWZpY2lhbCBmZWF0dXJlcyBvciBjb21wb25lbnRzLiBUaGVzZSBjb21wb25lbnRzIGFyZSBkZXNpZ25lZCB0byBjYXB0dXJlIHRoZSBtYXhpbXVtIGFtb3VudCBvZiBpbmZvcm1hdGlvbiAoaS5lLiB2YXJpYW5jZSkgaW4gdGhlIG9yaWdpbmFsIHZhcmlhYmxlcy4gQWxzbywgdGhlIGNvbXBvbmVudHMgYXJlIHN0YXRpc3RpY2FsbHkgaW5kZXBlbmRlbnQgZnJvbSBvbmUgYW5vdGhlci4gVGhpcyBtZWFucyB0aGF0IHRoZXkgY2FuIGJlIHVzZWQgdG8gY29tYmF0IGxhcmdlIGludGVyLXZhcmlhYmxlIGNvcnJlbGF0aW9ucyBpbiBhIGRhdGEgc2V0Lg0KDQpgYGB7ciBhZHZhbmNlZF9yZWNpcGUsIGZpZy5hc3A9MX0NCihhZHZhbmNlZF9yZWMgPC0NCiAgYmFzaWNfcmVjICU+JSANCiAgc3RlcF9pbnRlcmFjdCh0ZXJtcyA9IH4gcmVsZWFzZV95ZWFyOmVuZXJneSkgJT4lIA0KICBzdGVwX2ludGVyYWN0KHRlcm1zID0gfiByZWxlYXNlX3llYXI6Zm9sbG93ZXJzKSAlPiUgDQogIHN0ZXBfbm9ybWFsaXplKGFsbF9udW1lcmljX3ByZWRpY3RvcnMoKSkgJT4lIA0KICBzdGVwX3BjYShhbGxfbnVtZXJpY19wcmVkaWN0b3JzKCksIHRocmVzaG9sZCA9IDAuOSkpDQoNCnRpZHlfY29lZiA8LSB0aWR5KHByZXAoYWR2YW5jZWRfcmVjKSwgMTAsIHR5cGUgPSAiY29lZiIpDQoNCnRpZHlfY29lZiAlPiUgDQogIGZpbHRlcihjb21wb25lbnQgJWluJSBwYXN0ZTAoIlBDIiwxOjYpKSAlPiUgDQogIGdyb3VwX2J5KGNvbXBvbmVudCkgJT4lIA0KICBzbGljZV9tYXgoYWJzKHZhbHVlKSwgbiA9IDEyKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIG11dGF0ZSh0ZXJtcyA9IHRpZHl0ZXh0OjpyZW9yZGVyX3dpdGhpbih0ZXJtcywgYWJzKHZhbHVlKSwgY29tcG9uZW50KSkgJT4lIA0KICBnZ3Bsb3QoYWVzKGFicyh2YWx1ZSksIHRlcm1zLCBmaWxsID0gdmFsdWUgPiAwKSkgKw0KICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogIGZhY2V0X3dyYXAofiBjb21wb25lbnQsIHNjYWxlcyA9ICJmcmVlX3kiKSArDQogIHRpZHl0ZXh0OjpzY2FsZV95X3Jlb3JkZXJlZCgpICsNCiAgbGFicyh0aXRsZSA9ICJXaGF0IGZlYXR1cmVzIGRyaXZlIHRoZSB0b3AgNiBwcmluY2lwYWwgY29tcG9uZW50cz8iLA0KICAgICAgIHN1YnRpdGxlID0gIk5vdGUgdGhlIHByZXNlbmNlIG9mIHRoZSBpbnRlcmFjdGlvbnMuIFJlY2VudCwgaGlnaCBlbmVyZ3ksIGxvdWQgdHJhY2tzIGFyZSBtb3N0IHBvcHVsYXIuIiwNCiAgICAgICB5ID0gTlVMTCkNCiAgDQpiYWtlKHByZXAoYWR2YW5jZWRfcmVjKSwgbmV3X2RhdGEgPSBOVUxMKSAlPiUgDQogIGdncGxvdChhZXMoUEMwMSwgUEMwMiwgeiA9IHBvcHVsYXJpdHkpKSArDQogIHN0YXRfc3VtbWFyeV9oZXgoYWxwaGEgPSAwLjksIGJpbnMgPSA0MCkgKw0KICBzY2FsZV9maWxsX3ZpcmlkaXNfYyhvcHRpb24gPSAiRSIpICsNCiAgbGFicyhmaWxsID0gIlBvcHVsYXJpdHkiLA0KICAgICAgIHRpdGxlID0gIkZpcnN0IHR3byBwcmluY2lwYWwgY29tcG9uZW50cyIpDQogIA0KYGBgDQoNCiMjIFR1bmluZyBhbmQgUGVyZm9ybWFuY2UNCg0KVGhlIGNsZWFuIGFkdmFuY2VkIHByZXByb2Nlc3NvciB3aXRoIGludGVyYWN0aW9ucyBzaG91bGQgeWllbGQgYSBiZXR0ZXIgbW9kZWwuIExldCdzIHNlZS4NCg0KYGBge3IgeGdib29zdDMgYWR2YW5jZWQgc3BlY30NCih4Z2Jvb3N0X3NwZWMgPC0gYm9vc3RfdHJlZSh0cmVlcyA9IHR1bmUoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fbiA9IHR1bmUoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBsZWFybl9yYXRlID0gMC4wMiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmVlX2RlcHRoID0gdHVuZSgpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhbXBsZV9zaXplID0gdHVuZSgpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxvc3NfcmVkdWN0aW9uID0gdHVuZSgpKSAlPiUgDQogIHNldF9lbmdpbmUoInhnYm9vc3QiKSAlPiUNCiAgc2V0X21vZGUoInJlZ3Jlc3Npb24iKSkNCmBgYA0KDQpgYGB7ciB4Z2Jvb3N0MyBub2V2YWwsIGV2YWwgPSBGQUxTRX0NCmN2X3Jlc194Z2Jvb3N0IDwtDQogIHdvcmtmbG93KCkgJT4lIA0KICBhZGRfcmVjaXBlKGFkdmFuY2VkX3JlYykgJT4lIA0KICBhZGRfbW9kZWwoeGdib29zdF9zcGVjKSAlPiUgDQogIHR1bmVfcmFjZV9hbm92YSggICAgDQogICAgcmVzYW1wbGVzID0gZm9sZHMsDQogICAgZ3JpZCA9IDEwLA0KICAgIGNvbnRyb2wgPSBjb250cm9sX3JhY2UodmVyYm9zZSA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV9wcmVkID0gVFJVRSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzYXZlX3dvcmtmbG93ID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGV4dHJhY3QgPSBleHRyYWN0X21vZGVsLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgcGFyYWxsZWxfb3ZlciA9ICJyZXNhbXBsZXMiKSwNCiAgICBtZXRyaWNzID0gbXNldA0KKQ0KYGBgDQoNCmBgYHtyIHhnYm9vc3QzIG5vaW5jbHVkZSwgaW5jbHVkZSA9IEZBTFNFfQ0KaWYgKGZpbGUuZXhpc3RzKGhlcmU6OmhlcmUoImRhdGEiLCAic3BvdGlmeXhnYm9vc3RhZHZhbmNlZC5yZHMiKSkpIHsNCmN2X3Jlc194Z2Jvb3N0IDwtIHJlYWRfcmRzKGhlcmU6OmhlcmUoImRhdGEiLCAic3BvdGlmeXhnYm9vc3RhZHZhbmNlZC5yZHMiKSkNCn0gZWxzZSB7DQpjdl9yZXNfeGdib29zdCA8LQ0KICB3b3JrZmxvdygpICU+JSANCiAgYWRkX3JlY2lwZShhZHZhbmNlZF9yZWMpICU+JSANCiAgYWRkX21vZGVsKHhnYm9vc3Rfc3BlYykgJT4lIA0KICB0dW5lX3JhY2VfYW5vdmEoICAgIA0KICAgIHJlc2FtcGxlcyA9IGZvbGRzLA0KICAgIGdyaWQgPSAxMCwNCiAgICBjb250cm9sID0gY29udHJvbF9yYWNlKHZlcmJvc2UgPSBGQUxTRSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVfcHJlZCA9IFRSVUUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgc2F2ZV93b3JrZmxvdyA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICBleHRyYWN0ID0gZXh0cmFjdF9tb2RlbCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFsbGVsX292ZXIgPSAicmVzYW1wbGVzIiksDQogICAgbWV0cmljcyA9IG1zZXQNCikNCndyaXRlX3Jkcyhjdl9yZXNfeGdib29zdCwgaGVyZTo6aGVyZSgiZGF0YSIsICJzcG90aWZ5eGdib29zdGFkdmFuY2VkLnJkcyIpKQ0KfQ0KYGBgDQoNCmBgYHtyIHBlcmZvcm1hbmNlIGFkdmFuY2VkfQ0KYXV0b3Bsb3QoY3ZfcmVzX3hnYm9vc3QpDQoNCmNvbGxlY3RfbWV0cmljcyhjdl9yZXNfeGdib29zdCkgJT4lIA0KICBhcnJhbmdlKG1lYW4pDQoNCmBgYA0KDQpUaGUgYmVzdCBtZWFuIHJtc2UgYXZlcmFnZWQgYWNyb3NzIGZvbGRzIGlzIGFib3V0IDEyLjEuDQoNCmBgYHtyIHhnYm9vc3QzIGZpdH0NCnhnYl93Zl9iZXN0IDwtICAgDQogIHdvcmtmbG93KCkgJT4lIA0KICBhZGRfcmVjaXBlKGFkdmFuY2VkX3JlYykgJT4lIA0KICBhZGRfbW9kZWwoeGdib29zdF9zcGVjKSAlPiUgDQogIGZpbmFsaXplX3dvcmtmbG93KHNlbGVjdF9iZXN0KGN2X3Jlc194Z2Jvb3N0KSkNCg0KZml0X2Jlc3QgPC0geGdiX3dmX2Jlc3QgJT4lDQogIHBhcnNuaXA6OmZpdChkYXRhID0gdHJhaW5fZGYpDQoNCmF1Z21lbnQoZml0X2Jlc3QsIHRyYWluX2RmKSAlPiUgDQogIHNlbGVjdChpZCwgcG9wdWxhcml0eSwgLnByZWQpICU+JSANCiAgcm1zZSh0cnV0aCA9IHBvcHVsYXJpdHksIGVzdGltYXRlID0gLnByZWQpDQpgYGANCg0KV2UncmUgb3V0IG9mIHRpbWUuIFRoaXMgd2lsbCBiZSBhcyBnb29kIGFzIGl0IGdldHMuIE91ciBmaW5hbCBzdWJtaXNzaW9uOg0KDQpgYGB7ciB4Z2Jvb3N0MyB3cml0ZSwgZXZhbCA9IEZBTFNFfQ0KYXVnbWVudChmaXRfYmVzdCwgdGVzdF9kZikgJT4lIA0KICBzZWxlY3QoaWQsIHBvcHVsYXJpdHkgPSAucHJlZCkgJT4lIA0KICB3cml0ZV9jc3YoZmlsZSA9IHBhdGhfZXhwb3J0KQ0KYGBgDQoNCmBgYHtyIHhnYm9vc3QzIHN1Ym1pdCwgZXZhbCA9IEZBTFNFfQ0Kc2hlbGwoZ2x1ZTo6Z2x1ZSgna2FnZ2xlIGNvbXBldGl0aW9ucyBzdWJtaXQgLWMgeyBjb21wZXRpdGlvbl9uYW1lIH0gLWYgeyBwYXRoX2V4cG9ydCB9IC1tICJUaGlyZCBtb2RlbCInKSkNCmBgYA0KDQojIHstfQ0KDQotLS0tDQoNCkl0IHR1cm5zIG91dCB0aGF0IGFuIFJNU0UgaW4gdGhlIG5laWdoYm9ybm9vZCBvZiAxMCBpcyBjb21wZXRpdGl2ZSB3aXRoIHRoZSBvdGhlcnMgb24gdGhlIGxlYWRlcmJvYXJkLg0KDQoNCg==