Last updated: 2022-12-16

Checks: 7 0

Knit directory: myTidyTuesday/

This reproducible R Markdown analysis was created with workflowr (version 1.7.0). 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 f0ab5e8. 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:    data/.Rhistory
    Ignored:    data/2022_11_01.png
    Ignored:    data/2022_11_18.png
    Ignored:    data/CNHI_Excel_Chart.xlsx
    Ignored:    data/Chicago.rds
    Ignored:    data/CommunityTreemap.jpeg
    Ignored:    data/Community_Roles.jpeg
    Ignored:    data/ELL.zip
    Ignored:    data/FM_service_contour_current.zip
    Ignored:    data/SeriesReport-20220414171148_6c3b18.xlsx
    Ignored:    data/Weekly_Chicago_IL_Regular_Reformulated_Retail_Gasoline_Prices.csv
    Ignored:    data/YammerDigitalDataScienceMembership.xlsx
    Ignored:    data/YammerMemberPage.rds
    Ignored:    data/YammerMembers.rds
    Ignored:    data/application_id.feather
    Ignored:    data/df.rds
    Ignored:    data/fit_cohesion.rds
    Ignored:    data/fit_grammar.rds
    Ignored:    data/fit_phraseology.rds
    Ignored:    data/fit_syntax.rds
    Ignored:    data/fit_vocabulary.rds
    Ignored:    data/grainstocks.rds
    Ignored:    data/hike_data.rds
    Ignored:    data/lm_res.rds
    Ignored:    data/raw_contour.feather
    Ignored:    data/raw_weather.RData
    Ignored:    data/sample_submission.csv
    Ignored:    data/submission.csv
    Ignored:    data/test.csv
    Ignored:    data/train.csv
    Ignored:    data/us_states.rds
    Ignored:    data/us_states_hexgrid.geojson
    Ignored:    data/weatherstats_toronto_daily.csv

Untracked files:
    Untracked:  analysis/2022_09_01_kaggle_tabular_playground.qmd
    Untracked:  code/YammerReach.R
    Untracked:  code/autokeras.R
    Untracked:  code/chicago.R
    Untracked:  code/glmnet_test.R
    Untracked:  code/googleCompute.R
    Untracked:  code/work list batch targets.R
    Untracked:  environment.yml
    Untracked:  report.html

Unstaged changes:
    Modified:   analysis/2021_01_19_tidy_tuesday.Rmd
    Modified:   analysis/2021_03_24_tidy_tuesday.Rmd
    Deleted:    analysis/2021_04_20.Rmd
    Deleted:    analysis/2022_02_11_tabular_playground.Rmd
    Deleted:    analysis/2022_04_18.qmd
    Modified:   analysis/Survival.Rmd
    Modified:   analysis/_site.yml
    Modified:   code/_common.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/EnglishLanguageLearning.Rmd) and HTML (docs/EnglishLanguageLearning.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
html f0ab5e8 opus1993 2022-11-29 Build site.
Rmd c07b02a opus1993 2022-11-29 wflow_publish("analysis/EnglishLanguageLearning.Rmd")
Rmd 42d2421 opus1993 2022-11-29 add likert and resamples on all metrics
html b5a9c2b opus1993 2022-10-15 Build site.
html fad0136 opus1993 2022-10-15 Build site.
Rmd 73c07de opus1993 2022-10-15 Kaggle English Language Learning
html 66b10e0 opus1993 2022-10-15 Build site.
Rmd bb8b757 opus1993 2022-10-15 initial commit of Kaggle English Language Learning

The Kaggle Challenge presented here works with a dataset that comprises argumentative essays (the ELLIPSE corpus) written by 8th-12th grade English Language Learners (ELLs). The essays have been scored on six measures: cohesion, syntax, vocabulary, phraseology, grammar, and conventions.

Each measure represents a component of writing proficiency, ranging from 1.0 to 5.0 in increments of 0.5. Our task is to predict the score of each measure by essay.

This is the rubric that was used to grade the essays. Two people did the work independently, and then the scores were compared for alignment.

Preprocessing

Natural Language Processing techniques offer a wide variety of tools to approach this problem. The Kaggle host is requiring that the model run as a standalone, without internet assistance. They also ask for a parsimonous, explainable model.

We will start with exploring the predictive potential of the text count features, like numbers of words, distinct words, and spaces.

Unsupervised topic grouping categories may be useful for measures like conventions or grammar. In this case, we will start with Latent Dirichlet allocation (LDA).

Individual words may have predictive power, but they could be so sparse as to be difficult to separate from the background noise. Consider words like ain’t and phrases taken from other languages.

Bringing in a sentiment dictionary may add predictive power to some measures, along with helping to count miss-spellings. Word embeddings like Glove or Huggingface could also better characterize meaning.

Modeling

Many developers are tempted to jump into (CNN / LSTM) deep learning, but the number of essays is really pretty small for a deep learning run on their own. Another approach could leverage the pre-trained embeddings in one of the BERTs. The current Kaggle leaderboard is full of them. Even so, the standings will shift in a huge way after the full test set calculations appear because of overfitting and imbalance.

The GloVe pre-trained word vectors provide word embeddings created on existing document corpus, and are provided as a pre-processor using varying numbers of tokens. See Jeffrey Pennington, Richard Socher, and Christopher D. Manning. 2014. GloVe: Global Vectors for Word Representation. for details.

I spent a few evenings with the torch/brulee approach on tidymodels, but discovered that modeling time consumed would be significant and the results were not better than random forests on engineered features with case weights based on inverse proportions of the metric values.

I ultimately settled on the xgboost approach here. No doubt it can still overfit on specific words and text attributes, like the number of unique words.

One last point. I believe that the Essay Scoring is done by humans in a way where the metrics are judged together, and not entirely independently. In other words, low grammar and low cohesion are related.

suppressPackageStartupMessages({
library(tidyverse)
  
library(tidymodels)
library(text2vec) # for topic modeling

library(tidytext)
library(textrecipes)

})

tidymodels::tidymodels_prefer()

theme_set(theme_minimal())

Let’s read the data from Kaggle’s csv’s into dataframes.

train_essays_raw <- read_csv(here::here("data","train.csv"),
                         show_col_types = FALSE) 

submit_essays_raw <- read_csv(here::here("data","test.csv"),
                          show_col_types = FALSE) 

outcomes = names(train_essays_raw)[3:8]

dim(train_essays_raw)
[1] 3911    8

The essay metrics score distributions resemble ordinal Likert scales. One way to illustrate the counts at each level is this bar chart:

stage1 <- train_essays_raw |>
  select(cohesion:conventions) |>
  pivot_longer(cols = everything(),
               names_to = "metric",
               values_to = "ans") |>
  group_by(ans, metric) |>
  summarize(n = n(),
            .groups = "drop") |>
  group_by(metric) |>
  mutate(per = n / sum(n)) |>
  mutate(
    text = paste0(formatC(
      100 * per, format = "f", digits = 0
    ), "%"),
    cs = cumsum(per),
    offset = sum(per[1:(floor(n() / 2))]) + (n() %% 2) * 0.5 * (per[ceiling(n() /
                                                                              2)]),
    xmax = -offset + cs,
    xmin = xmax - per
  ) |>
  ungroup()

gap <- 0.2

stage2 <- stage1 %>%
  left_join(
    stage1 %>%
      group_by(metric) %>%
      summarize(max.xmax = max(xmax)) %>%
      mutate(r = row_number(max.xmax)),
    by = "metric"
  ) %>%
  arrange(desc(r)) %>%
  mutate(ymin = r - (1 - gap) / 2,
         ymax = r + (1 - gap) / 2)

ggplot(stage2) +
  geom_vline(xintercept = 0) +
  geom_rect(aes(
    xmin = xmin,
    xmax = xmax,
    ymin = ymin,
    ymax = ymax,
    fill = factor(ans)
  )) +
  geom_text(aes(
    x = (xmin + xmax) / 2,
    y = (ymin + ymax) / 2,
    label = text
  ),
  size = 3,
  check_overlap = TRUE) +
  scale_x_continuous(
    "",
    labels = percent,
    breaks = seq(-0.6, 0.65, len = 6),
    limits = c(-0.6, 0.65)
  ) +   scale_y_continuous(
    "",
    breaks = 1:n_distinct(stage2$metric),
    labels = rev(stage2 %>% distinct(metric) %>% .$metric)
  ) +
  scale_fill_brewer("Score", palette = "BrBG") +
  labs(title = "Training set Essay Ratings")

Essays with more words, or more sentences, do not necessarily score better.

te_long <- train_essays_raw |>
  pivot_longer(cols = cohesion:conventions,
               names_to = "metric",
               values_to = "value") |>
  mutate(metric = as.factor(metric),
         value = as.factor(value))

te_long |> 
  group_by(n_words = ggplot2::cut_interval(
    tokenizers::count_words(full_text), 
    length = 200),
    metric, value) |> 
  summarise(`Number of essays` = n(),
            .groups = "drop") |> 
  ggplot(aes(n_words, `Number of essays`, fill = as.factor(value))) +
  geom_col() +
  scale_x_discrete(guide = guide_axis(n.dodge = 2)) +
  facet_wrap(vars(metric)) +
  scale_fill_brewer("Score", palette = "BrBG") +
  labs(x = "Number of words per essay",
       y = "Number of essays",
       fill = "Score")

te_long |> 
  group_by(n_words = ggplot2::cut_interval(
    tokenizers::count_sentences(full_text), length = 20),
    metric, value) |> 
  summarise(`Number of essays` = n(),
            .groups = "drop") |> 
  ggplot(aes(n_words, `Number of essays`, fill = as.factor(value))) +
  geom_col() +
  scale_x_discrete(guide = guide_axis(n.dodge = 2)) +
  facet_wrap(vars(metric)) +
  scale_fill_brewer("Score", palette = "BrBG") +
  labs(x = "Number of sentences per essay",
       y = "Number of essays",
       fill = "Score")

What words from the dialogue have the highest log odds of coming from each level of each outcome? Do the individual words have predictive power?

plot_log_odds <- function(outcome = "cohesion"){

train_essays_raw |>
  tidytext::unnest_tokens(word, full_text) |> 
  count(level = factor(.data[[outcome]]), word, sort = TRUE) |>   
  tidylo::bind_log_odds(level, word, n) |> 
  filter(n > 20) |> 
  group_by(level) |> 
  slice_max(log_odds_weighted, n = 10) |> 
  mutate(word = reorder_within(word, log_odds_weighted, level)) %>%
  ggplot(aes(log_odds_weighted, word, fill = level)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(vars(level), scales = "free") +
  scale_fill_brewer("Score", palette = "BrBG") +
  scale_y_reordered() +
  labs(y = NULL, title = glue::glue("{outcome} log odds words"))  
    
}

map(outcomes, plot_log_odds)
[[1]]


[[2]]


[[3]]


[[4]]


[[5]]


[[6]]

To some extent, the answer may be yes.

Let’s also take a look at outcome pairwise correlations.

train_essays_raw |> 
  corrr::correlate(
    quiet = TRUE
  ) %>%
  corrr::rearrange() %>%
  corrr::shave() %>%
  corrr::rplot(print_cor = TRUE,
               colors = brewer_pal(palette = "BrBG")(5)) +
  scale_x_discrete(guide = guide_axis(n.dodge = 3))

Avoiding overfitting to the training data is critical to achieving a strong score. We are going to use resampling to have some indication that our model generalizes to new essays. Care must be exercised to be sure that members of the hold out folds are not also found in the training folds.

Latent Dirichlet allocation (LDA) is an unsupervised generative statistical model that explains a set of observations through unobserved groups, and the content of each group may explain why some parts of the data are similar.

I’d like to explore the use of inverse probability weights because there are so few essays with scores at the highest and lowest levels. When survey respondents have different probabilities of selection, (inverse) probability weights help reduce bias in the results.

I am making us of metaprogramming techniques to pass text vector column names into the formula and case weights functions to re-use them for each metric.

tokens = text2vec::word_tokenizer(tolower(train_essays_raw$full_text))

it = text2vec::itoken(tokens, ids = train_essays_raw$text_id, progressbar = FALSE)

v = text2vec::create_vocabulary(it)

dtm = text2vec::create_dtm(it, text2vec::vocab_vectorizer(v), type = "RsparseMatrix")

lda_model <- text2vec::LDA$new(n_topics = 30)

case_weight_builder <- function(data, outcome) {
  data %>%
    inner_join(data %>%
                 count(.data[[outcome]],
                       name = "case_wts"),
               by = glue::glue("{ outcome }")) %>%
    mutate(case_wts = importance_weights(max(case_wts) / case_wts))
}

recipe_builder <- function(outcome = "cohesion") {
  rec <- recipe(
    formula(glue::glue("{ outcome } ~ .")),
    data = train_essays_raw |>
      select({
        {
          outcome
        }
      }, full_text) |>
      case_weight_builder(outcome)
  ) |>
    step_textfeature(full_text,
                     keep_original_cols = TRUE) |>
    step_rename_at(starts_with("textfeature_"),
                   fn = ~ gsub("textfeature_full_text_", "", .)) %>%
    step_tokenize(full_text) %>%
    step_lda(full_text,
             lda_models = lda_model,
             keep_original_cols = TRUE) %>%
    step_word_embeddings(
      full_text,
      aggregation = "sum",
      embeddings = textdata::embedding_glove27b(dimensions = 200)
    ) |>
    step_zv(all_numeric_predictors()) |>
    step_normalize(all_numeric_predictors())
  
  return(rec)
  
}


multiclass_recipe_builder <- function(outcome = "cohesion") {
  rec <- recipe(formula(glue::glue("{ outcome } ~ .")),
                data = classification_train_df) |>
    step_textfeature(full_text,
                     keep_original_cols = TRUE) |>
    step_rename_at(starts_with("textfeature_"),
                   fn = ~ gsub("textfeature_full_text_", "", .)) %>%
    step_tokenize(full_text) %>%
    step_lda(full_text,
             lda_models = lda_model,
             keep_original_cols = TRUE) %>%
    step_word_embeddings(
      full_text,
      aggregation = "sum",
      embeddings = textdata::embedding_glove27b(dimensions = 200)
    ) |>
    step_zv(all_numeric_predictors()) |>
    step_normalize(all_numeric_predictors())
  
  return(rec)
  
}

plot_preds <- function(dat, outcome){

dat |> 
  ggplot(aes(x = {{outcome}}, y = .pred)) +
  geom_point(alpha = 0.15) +
  geom_abline(color = "red") +
  coord_obs_pred() 

}

As mentioned above, the model specification is xgboost for regression to predict a continuous outcome that resembles ordinal classes.

xgb_spec <-
  boost_tree(
    mtry = 50,  # 75L
    trees = 1000L,
    tree_depth = 9, # 6L
    learn_rate = 0.01,  # originally 0.1
    min_n = 39L,  # 20L
    loss_reduction = 0
  ) |> 
  set_engine('xgboost') |> 
  set_mode('regression')

svm_spec <- svm_linear() |> 
  set_engine("LiblineaR") |> 
  set_mode("classification")  

To speed the computations let’s enable a parallel backend.

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

Modeling

Cohesion

We fit for cohesion first using an xgboost regression, using case weights to adjust for the frequency of occurrence of each value of cohesion.

outcome <- outcomes[1]

regression_train_df <- train_essays_raw  |> 
                select(!!outcome, full_text) |> 
                case_weight_builder(outcome)

regression_wf <- workflow(recipe_builder(outcome = outcome), xgb_spec) |> 
       add_case_weights(case_wts)

folds <- vfold_cv(regression_train_df, strata = {{outcome}})

set.seed(42)  
rs <- fit_resamples(
  regression_wf,
  folds,
  control = control_resamples(save_pred = TRUE))
as(<dgTMatrix>, "dgCMatrix") is deprecated since Matrix 1.5-0; do as(., "CsparseMatrix") instead
collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  plot_preds(.data[[outcome]]) +
  labs(y = "Predicted",
       title = paste0(outcome, " predictions against essays in held out folds"),
       subtitle = "The highest and lowest essays are not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = .data[[outcome]] - .pred) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text)
regression_fit <- parsnip::fit(regression_wf, 
                     regression_train_df)

Identifying examples with especially poor performance can help us follow up and investigate why these specific predictions are poor. Conceptually, its easy for a baseline know-nothing model to assign all essays to the median score of 3. The predictive power is in the ability to model the essays that are not 3 into buckets higher and lower than 3.

Because the ratings are a form of ordinal value, or even a likert scale, we will ensemble a second classification model that includes the output of the regression.

classification_train_df <- train_essays_raw  |> 
                select({{outcome}}, full_text) |> 
                bind_cols(
                  predict(
                    regression_fit,
                    regression_train_df
                  )
                ) |> 
               rename(regression_pred = .pred) |> 
               mutate({{outcome}} := factor(.data[[outcome]]))

classification_wf <- workflow(multiclass_recipe_builder(outcome = outcome), svm_spec) 

folds <- vfold_cv(classification_train_df, strata = !!outcome)

set.seed(42)  
rs <- fit_resamples(
  classification_wf,
  folds,
  metrics = metric_set(kap, accuracy),
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  ggplot(aes(x = .data[[outcome]], y = abs(as.numeric(.data[[outcome]]) - as.numeric(.pred_class))/2)) +
  geom_violin() +
  scale_y_continuous(breaks = seq(-5,5,0.5)) +
  labs(y = "Residuals",
       title = "{{outcome}} Residual errors for essays in held out folds",
       subtitle = "The highest and lowest essays are still not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = as.numeric(.data[[outcome]]) - as.numeric(.pred_class)) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text, {{outcome}})
collect_predictions(rs) |> 
  rmse(truth = as.numeric(.data[[outcome]])/2, estimate = as.numeric(.pred_class)/2)

Results here aren’t great, but they are more are less competitive with the leaderboard figures.

The final fitting ensembles both the regression and classification fits, and makes a prediction on the submission essays.

classification_fit <- parsnip::fit(classification_wf,
                                   classification_train_df)

extract_fit_engine(regression_fit) |> 
  vip::vip(num_features = 20)

submission <- predict(
  classification_fit,
  
  submit_essays_raw |>
    bind_cols(predict(regression_fit, submit_essays_raw)) |>
    rename(regression_pred = .pred)
) |>
  transmute({{outcome}} := .pred_class)
Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?

Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?
submission

Syntax

We fit for syntax first using an xgboost regression, using case weights to adjust for the frequency of occurrence of each value of syntax.

outcome <- outcomes[2]

regression_train_df <- train_essays_raw  |> 
                select(!!outcome, full_text) |> 
                case_weight_builder(outcome)

regression_wf <- workflow(recipe_builder(outcome = outcome), xgb_spec) |> 
       add_case_weights(case_wts)

folds <- vfold_cv(regression_train_df, strata = {{outcome}})

set.seed(42)  
rs <- fit_resamples(
  regression_wf,
  folds,
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  plot_preds(.data[[outcome]]) +
  labs(y = "Predicted",
       title = paste0(outcome, " predictions against essays in held out folds"),
       subtitle = "The highest and lowest essays are not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = .data[[outcome]] - .pred) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text)
regression_fit <- parsnip::fit(regression_wf, 
                     regression_train_df)

Identifying examples with especially poor performance can help us follow up and investigate why these specific predictions are poor. Conceptually, its easy for a baseline know-nothing model to assign all essays to the median score of 3. The predictive power is in the ability to model the essays that are not 3 into buckets higher and lower than 3.

Because the ratings are a form of ordinal value, or even a likert scale, we will ensemble a second classification model that includes the output of the regression.

classification_train_df <- train_essays_raw  |> 
                select({{outcome}}, full_text) |> 
                bind_cols(
                  predict(
                    regression_fit,
                    regression_train_df
                  )
                ) |> 
               rename(regression_pred = .pred) |> 
               mutate({{outcome}} := factor(.data[[outcome]]))

classification_wf <- workflow(multiclass_recipe_builder(outcome = outcome), svm_spec) 

folds <- vfold_cv(classification_train_df, strata = !!outcome)

set.seed(42)  
rs <- fit_resamples(
  classification_wf,
  folds,
  metrics = metric_set(kap, accuracy),
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  ggplot(aes(x = .data[[outcome]], y = abs(as.numeric(.data[[outcome]]) - as.numeric(.pred_class))/2)) +
  geom_violin() +
  scale_y_continuous(breaks = seq(-5,5,0.5)) +
  labs(y = "Residuals",
       title = "{{outcome}} Residual errors for essays in held out folds",
       subtitle = "The highest and lowest essays are still not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = as.numeric(.data[[outcome]]) - as.numeric(.pred_class)) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text, {{outcome}})
collect_predictions(rs) |> 
  rmse(truth = as.numeric(.data[[outcome]])/2, estimate = as.numeric(.pred_class)/2)

Results here aren’t great, but they are more are less competitive with the leaderboard figures.

The final fitting ensembles both the regression and classification fits, and makes a prediction on the submission essays.

classification_fit <- parsnip::fit(classification_wf,
                                   classification_train_df)

extract_fit_engine(regression_fit) |> 
  vip::vip(num_features = 20)

submission <- predict(
  classification_fit,
  
  submit_essays_raw |>
    bind_cols(predict(regression_fit, submit_essays_raw)) |>
    rename(regression_pred = .pred)
) |>
  transmute({{outcome}} := .pred_class) |>
  bind_cols(submission)
Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?

Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?
submission

Vocabulary

We fit for vocabulary first using an xgboost regression, using case weights to adjust for the frequency of occurrence of each value of vocabulary.

outcome <- outcomes[3]

regression_train_df <- train_essays_raw  |> 
                select(!!outcome, full_text) |> 
                case_weight_builder(outcome)

regression_wf <- workflow(recipe_builder(outcome = outcome), xgb_spec) |> 
       add_case_weights(case_wts)

folds <- vfold_cv(regression_train_df, strata = {{outcome}})

set.seed(42)  
rs <- fit_resamples(
  regression_wf,
  folds,
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  plot_preds(.data[[outcome]]) +
  labs(y = "Predicted",
       title = paste0(outcome, " predictions against essays in held out folds"),
       subtitle = "The highest and lowest essays are not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = .data[[outcome]] - .pred) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text)
regression_fit <- parsnip::fit(regression_wf, 
                     regression_train_df)

Identifying examples with especially poor performance can help us follow up and investigate why these specific predictions are poor. Conceptually, its easy for a baseline know-nothing model to assign all essays to the median score of 3. The predictive power is in the ability to model the essays that are not 3 into buckets higher and lower than 3.

Because the ratings are a form of ordinal value, or even a likert scale, we will ensemble a second classification model that includes the output of the regression.

classification_train_df <- train_essays_raw  |> 
                select({{outcome}}, full_text) |> 
                bind_cols(
                  predict(
                    regression_fit,
                    regression_train_df
                  )
                ) |> 
               rename(regression_pred = .pred) |> 
               mutate({{outcome}} := factor(.data[[outcome]]))

classification_wf <- workflow(multiclass_recipe_builder(outcome = outcome), svm_spec) 

folds <- vfold_cv(classification_train_df, strata = !!outcome)

set.seed(42)  
rs <- fit_resamples(
  classification_wf,
  folds,
  metrics = metric_set(kap, accuracy),
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  ggplot(aes(x = .data[[outcome]], y = abs(as.numeric(.data[[outcome]]) - as.numeric(.pred_class))/2)) +
  geom_violin() +
  scale_y_continuous(breaks = seq(-5,5,0.5)) +
  labs(y = "Residuals",
       title = "{{outcome}} Residual errors for essays in held out folds",
       subtitle = "The highest and lowest essays are still not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = as.numeric(.data[[outcome]]) - as.numeric(.pred_class)) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text, {{outcome}})
collect_predictions(rs) |> 
  rmse(truth = as.numeric(.data[[outcome]])/2, estimate = as.numeric(.pred_class)/2)

Results here aren’t great, but they are more are less competitive with the leaderboard figures.

The final fitting ensembles both the regression and classification fits, and makes a prediction on the submission essays.

classification_fit <- parsnip::fit(classification_wf,
                                   classification_train_df)

extract_fit_engine(regression_fit) |> 
  vip::vip(num_features = 20)

submission <- predict(
  classification_fit,
  
  submit_essays_raw |>
    bind_cols(predict(regression_fit, submit_essays_raw)) |>
    rename(regression_pred = .pred)
) |>
  transmute({{outcome}} := .pred_class) |>
  bind_cols(submission)
Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?

Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?
submission

Phraseology

We fit for phraseology first using an xgboost regression, using case weights to adjust for the frequency of occurrence of each value of phraseology.

outcome <- outcomes[4]

regression_train_df <- train_essays_raw  |> 
                select(!!outcome, full_text) |> 
                case_weight_builder(outcome)

regression_wf <- workflow(recipe_builder(outcome = outcome), xgb_spec) |> 
       add_case_weights(case_wts)

folds <- vfold_cv(regression_train_df, strata = {{outcome}})

set.seed(42)  
rs <- fit_resamples(
  regression_wf,
  folds,
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  plot_preds(.data[[outcome]]) +
  labs(y = "Predicted",
       title = paste0(outcome, " predictions against essays in held out folds"),
       subtitle = "The highest and lowest essays are not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = .data[[outcome]] - .pred) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text)
regression_fit <- parsnip::fit(regression_wf, 
                     regression_train_df)

Identifying examples with especially poor performance can help us follow up and investigate why these specific predictions are poor. Conceptually, its easy for a baseline know-nothing model to assign all essays to the median score of 3. The predictive power is in the ability to model the essays that are not 3 into buckets higher and lower than 3.

Because the ratings are a form of ordinal value, or even a likert scale, we will ensemble a second classification model that includes the output of the regression.

classification_train_df <- train_essays_raw  |> 
                select({{outcome}}, full_text) |> 
                bind_cols(
                  predict(
                    regression_fit,
                    regression_train_df
                  )
                ) |> 
               rename(regression_pred = .pred) |> 
               mutate({{outcome}} := factor(.data[[outcome]]))

classification_wf <- workflow(multiclass_recipe_builder(outcome = outcome), svm_spec) 

folds <- vfold_cv(classification_train_df, strata = !!outcome)

set.seed(42)  
rs <- fit_resamples(
  classification_wf,
  folds,
  metrics = metric_set(kap, accuracy),
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  ggplot(aes(x = .data[[outcome]], y = abs(as.numeric(.data[[outcome]]) - as.numeric(.pred_class))/2)) +
  geom_violin() +
  scale_y_continuous(breaks = seq(-5,5,0.5)) +
  labs(y = "Residuals",
       title = "{{outcome}} Residual errors for essays in held out folds",
       subtitle = "The highest and lowest essays are still not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = as.numeric(.data[[outcome]]) - as.numeric(.pred_class)) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text, {{outcome}})
collect_predictions(rs) |> 
  rmse(truth = as.numeric(.data[[outcome]])/2, estimate = as.numeric(.pred_class)/2)

Results here aren’t great, but they are more are less competitive with the leaderboard figures.

The final fitting ensembles both the regression and classification fits, and makes a prediction on the submission essays.

classification_fit <- parsnip::fit(classification_wf,
                                   classification_train_df)

extract_fit_engine(regression_fit) |> 
  vip::vip(num_features = 20)

submission <- predict(
  classification_fit,
  
  submit_essays_raw |>
    bind_cols(predict(regression_fit, submit_essays_raw)) |>
    rename(regression_pred = .pred)
) |>
  transmute({{outcome}} := .pred_class) |>
  bind_cols(submission)
Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?

Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?
submission

Grammar

We fit for grammar first using an xgboost regression, using case weights to adjust for the frequency of occurrence of each value of grammar.

outcome <- outcomes[5]

regression_train_df <- train_essays_raw  |> 
                select(!!outcome, full_text) |> 
                case_weight_builder(outcome)

regression_wf <- workflow(recipe_builder(outcome = outcome), xgb_spec) |> 
       add_case_weights(case_wts)

folds <- vfold_cv(regression_train_df, strata = {{outcome}})

set.seed(42)  
rs <- fit_resamples(
  regression_wf,
  folds,
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  plot_preds(.data[[outcome]]) +
  labs(y = "Predicted",
       title = paste0(outcome, " predictions against essays in held out folds"),
       subtitle = "The highest and lowest essays are not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = .data[[outcome]] - .pred) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text)
regression_fit <- parsnip::fit(regression_wf, 
                     regression_train_df)

Identifying examples with especially poor performance can help us follow up and investigate why these specific predictions are poor. Conceptually, its easy for a baseline know-nothing model to assign all essays to the median score of 3. The predictive power is in the ability to model the essays that are not 3 into buckets higher and lower than 3.

Because the ratings are a form of ordinal value, or even a likert scale, we will ensemble a second classification model that includes the output of the regression.

classification_train_df <- train_essays_raw  |> 
                select({{outcome}}, full_text) |> 
                bind_cols(
                  predict(
                    regression_fit,
                    regression_train_df
                  )
                ) |> 
               rename(regression_pred = .pred) |> 
               mutate({{outcome}} := factor(.data[[outcome]]))

classification_wf <- workflow(multiclass_recipe_builder(outcome = outcome), svm_spec) 

folds <- vfold_cv(classification_train_df, strata = !!outcome)

set.seed(42)  
rs <- fit_resamples(
  classification_wf,
  folds,
  metrics = metric_set(kap, accuracy),
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  ggplot(aes(x = .data[[outcome]], y = abs(as.numeric(.data[[outcome]]) - as.numeric(.pred_class))/2)) +
  geom_violin() +
  scale_y_continuous(breaks = seq(-5,5,0.5)) +
  labs(y = "Residuals",
       title = "{{outcome}} Residual errors for essays in held out folds",
       subtitle = "The highest and lowest essays are still not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = as.numeric(.data[[outcome]]) - as.numeric(.pred_class)) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text, {{outcome}})
collect_predictions(rs) |> 
  rmse(truth = as.numeric(.data[[outcome]])/2, estimate = as.numeric(.pred_class)/2)

Results here aren’t great, but they are more are less competitive with the leaderboard figures.

The final fitting ensembles both the regression and classification fits, and makes a prediction on the submission essays.

classification_fit <- parsnip::fit(classification_wf,
                                   classification_train_df)

extract_fit_engine(regression_fit) |> 
  vip::vip(num_features = 20)

submission <- predict(
  classification_fit,
  
  submit_essays_raw |>
    bind_cols(predict(regression_fit, submit_essays_raw)) |>
    rename(regression_pred = .pred)
) |>
  transmute({{outcome}} := .pred_class) |>
  bind_cols(submission)
Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?

Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?
submission

Conventions

We fit for conventions first using an xgboost regression, using case weights to adjust for the frequency of occurrence of each value of conventions.

outcome <- outcomes[6]

regression_train_df <- train_essays_raw  |> 
                select(!!outcome, full_text) |> 
                case_weight_builder(outcome)

regression_wf <- workflow(recipe_builder(outcome = outcome), xgb_spec) |> 
       add_case_weights(case_wts)

folds <- vfold_cv(regression_train_df, strata = {{outcome}})

set.seed(42)  
rs <- fit_resamples(
  regression_wf,
  folds,
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  plot_preds(.data[[outcome]]) +
  labs(y = "Predicted",
       title = paste0(outcome, " predictions against essays in held out folds"),
       subtitle = "The highest and lowest essays are not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = .data[[outcome]] - .pred) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text)
regression_fit <- parsnip::fit(regression_wf, 
                     regression_train_df)

Identifying examples with especially poor performance can help us follow up and investigate why these specific predictions are poor. Conceptually, its easy for a baseline know-nothing model to assign all essays to the median score of 3. The predictive power is in the ability to model the essays that are not 3 into buckets higher and lower than 3.

Because the ratings are a form of ordinal value, or even a likert scale, we will ensemble a second classification model that includes the output of the regression.

classification_train_df <- train_essays_raw  |> 
                select({{outcome}}, full_text) |> 
                bind_cols(
                  predict(
                    regression_fit,
                    regression_train_df
                  )
                ) |> 
               rename(regression_pred = .pred) |> 
               mutate({{outcome}} := factor(.data[[outcome]]))

classification_wf <- workflow(multiclass_recipe_builder(outcome = outcome), svm_spec) 

folds <- vfold_cv(classification_train_df, strata = !!outcome)

set.seed(42)  
rs <- fit_resamples(
  classification_wf,
  folds,
  metrics = metric_set(kap, accuracy),
  control = control_resamples(save_pred = TRUE))

collect_metrics(rs) |> arrange(mean)
collect_predictions(rs) |> 
  ggplot(aes(x = .data[[outcome]], y = abs(as.numeric(.data[[outcome]]) - as.numeric(.pred_class))/2)) +
  geom_violin() +
  scale_y_continuous(breaks = seq(-5,5,0.5)) +
  labs(y = "Residuals",
       title = "{{outcome}} Residual errors for essays in held out folds",
       subtitle = "The highest and lowest essays are still not predicted well")

train_essays_raw[

collect_predictions(rs) |> 
  mutate(residual = as.numeric(.data[[outcome]]) - as.numeric(.pred_class)) |> 
  arrange(desc(abs(residual))) |> 
  slice_head(n = 5) |> 
  pull(.row)

, ] |> 
  select(full_text, {{outcome}})
collect_predictions(rs) |> 
  rmse(truth = as.numeric(.data[[outcome]])/2, estimate = as.numeric(.pred_class)/2)

Results here aren’t great, but they are more are less competitive with the leaderboard figures.

The final fitting ensembles both the regression and classification fits, and makes a prediction on the submission essays.

classification_fit <- parsnip::fit(classification_wf,
                                   classification_train_df)

extract_fit_engine(regression_fit) |> 
  vip::vip(num_features = 20)

submission <- predict(
  classification_fit,
  
  submit_essays_raw |>
    bind_cols(predict(regression_fit, submit_essays_raw)) |>
    rename(regression_pred = .pred)
) |>
  transmute({{outcome}} := .pred_class) |>
  bind_cols(submission)
Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?

Warning in get_dtm(corp): dtm has 0 rows. Empty iterator?
submission

The Submission

Kaggle’s system runs the workbook twice. The first time is on the tiny three line public test dataset here. The second time is on a much much larger hidden test dataset. As a check to simulate how the hidden datset might fit, we could re-fit on the train dataset text across all of the fits.

submission
# write_csv(submission, here::here("data", "submission.csv"))

Outcome

Not only was this exercise a good study of Likert evaluation data, but also of NLP techniques and of statistical resampling to assure that the model performs on unseen data. The resulting models here lack the predictive power needed for production use.


sessionInfo()
R version 4.2.2 (2022-10-31 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 22621)

Matrix products: default

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

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

other attached packages:
 [1] LiblineaR_2.10-22  xgboost_1.6.0.1    textfeatures_0.3.3 textrecipes_1.0.1 
 [5] tidytext_0.3.4     text2vec_0.6.3     yardstick_1.1.0    workflowsets_1.0.0
 [9] workflows_1.1.2    tune_1.0.1         rsample_1.1.0      recipes_1.0.3     
[13] parsnip_1.0.3      modeldata_1.0.1    infer_1.0.4        dials_1.1.0       
[17] scales_1.2.1       broom_1.0.1        tidymodels_1.0.0   forcats_0.5.2     
[21] stringr_1.5.0      dplyr_1.0.10       purrr_0.3.5        readr_2.1.3       
[25] tidyr_1.2.1        tibble_3.1.8       ggplot2_3.4.0      tidyverse_1.3.2   
[29] workflowr_1.7.0   

loaded via a namespace (and not attached):
  [1] readxl_1.4.1           backports_1.4.1        splines_4.2.2         
  [4] listenv_0.8.0          SnowballC_0.7.0        tidylo_0.2.0          
  [7] digest_0.6.30          ca_0.71.1              foreach_1.5.2         
 [10] htmltools_0.5.3        float_0.3-0            fansi_1.0.3           
 [13] magrittr_2.0.3         memoise_2.0.1          googlesheets4_1.0.1   
 [16] tzdb_0.3.0             globals_0.16.2         modelr_0.1.10         
 [19] gower_1.0.0            vroom_1.6.0            hardhat_1.2.0         
 [22] timechange_0.1.1       colorspace_2.0-3       vip_0.3.2             
 [25] rappdirs_0.3.3         rvest_1.0.3            haven_2.5.1           
 [28] xfun_0.35              callr_3.7.3            crayon_1.5.2          
 [31] jsonlite_1.8.4         survival_3.4-0         iterators_1.0.14      
 [34] glue_1.6.2             registry_0.5-1         gtable_0.3.1          
 [37] gargle_1.2.1           ipred_0.9-13           future.apply_1.10.0   
 [40] mlapi_0.1.1            DBI_1.1.3              Rcpp_1.0.9            
 [43] GPfit_1.0-8            bit_4.0.5              lava_1.7.0            
 [46] prodlim_2019.11.13     httr_1.4.4             RColorBrewer_1.1-3    
 [49] ellipsis_0.3.2         farver_2.1.1           pkgconfig_2.0.3       
 [52] nnet_7.3-18            sass_0.4.4             dbplyr_2.2.1          
 [55] utf8_1.2.2             here_1.0.1             labeling_0.4.2        
 [58] tidyselect_1.2.0       rlang_1.0.6            DiceDesign_1.9        
 [61] later_1.3.0            munsell_0.5.0          cellranger_1.1.0      
 [64] tools_4.2.2            cachem_1.0.6           cli_3.4.1             
 [67] corrr_0.4.4            generics_0.1.3         rsparse_0.5.1         
 [70] evaluate_0.18          fastmap_1.1.0          yaml_2.3.6            
 [73] textdata_0.4.4         processx_3.8.0         RhpcBLASctl_0.21-247.1
 [76] knitr_1.41             bit64_4.0.5            fs_1.5.2              
 [79] lgr_0.4.4              future_1.29.0          whisker_0.4.1         
 [82] xml2_1.3.3             tokenizers_0.2.3       compiler_4.2.2        
 [85] rstudioapi_0.14        reprex_2.0.2           lhs_1.1.5             
 [88] bslib_0.4.1            stringi_1.7.8          highr_0.9             
 [91] ps_1.7.2               lattice_0.20-45        Matrix_1.5-3          
 [94] conflicted_1.1.0       vctrs_0.5.1            pillar_1.8.1          
 [97] lifecycle_1.0.3        furrr_0.3.1            jquerylib_0.1.4       
[100] data.table_1.14.6      seriation_1.4.0        httpuv_1.6.6          
[103] R6_2.5.1               TSP_1.2-1              promises_1.2.0.1      
[106] gridExtra_2.3          janeaustenr_1.0.0      parallelly_1.32.1     
[109] codetools_0.2-18       MASS_7.3-58.1          assertthat_0.2.1      
[112] rprojroot_2.0.3        withr_2.5.0            parallel_4.2.2        
[115] hms_1.1.2              grid_4.2.2             rpart_4.1.19          
[118] timeDate_4021.106      class_7.3-20           rmarkdown_2.18        
[121] googledrive_2.0.0      git2r_0.30.1           getPass_0.2-2         
[124] lubridate_1.9.0       
LS0tDQp0aXRsZTogIkthZ2dsZSBGZWVkYmFjayBQcml6ZSAtIEVuZ2xpc2ggTGFuZ3VhZ2UgTGVhcm5pbmciDQphdXRob3I6ICJKaW0gR3J1bWFuIg0KZGF0ZTogIk9jdG9iZXIgMTUsIDIwMjIiDQpvdXRwdXQ6DQogIHdvcmtmbG93cjo6d2Zsb3dfaHRtbDoNCiAgICB0b2M6IG5vDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGRmX3ByaW50OiBwYWdlZA0KZWRpdG9yX29wdGlvbnM6DQogIGNodW5rX291dHB1dF90eXBlOiBjb25zb2xlDQotLS0NCg0KVGhlIEthZ2dsZSBDaGFsbGVuZ2UgcHJlc2VudGVkIGhlcmUgd29ya3Mgd2l0aCBhIGRhdGFzZXQgdGhhdCBjb21wcmlzZXMgYXJndW1lbnRhdGl2ZSBlc3NheXMgKHRoZSBFTExJUFNFIGNvcnB1cykgd3JpdHRlbiBieSA4dGgtMTJ0aCBncmFkZSBFbmdsaXNoIExhbmd1YWdlIExlYXJuZXJzIChFTExzKS4gVGhlIGVzc2F5cyBoYXZlIGJlZW4gc2NvcmVkIG9uIHNpeCBtZWFzdXJlczogKipjb2hlc2lvbiwgc3ludGF4LCB2b2NhYnVsYXJ5LCBwaHJhc2VvbG9neSwgZ3JhbW1hciwqKiBhbmQgKipjb252ZW50aW9ucyoqLg0KDQpFYWNoIG1lYXN1cmUgcmVwcmVzZW50cyBhIGNvbXBvbmVudCBvZiB3cml0aW5nIHByb2ZpY2llbmN5LCByYW5naW5nIGZyb20gMS4wIHRvIDUuMCBpbiBpbmNyZW1lbnRzIG9mIDAuNS4gT3VyIHRhc2sgaXMgdG8gcHJlZGljdCB0aGUgc2NvcmUgb2YgZWFjaCBtZWFzdXJlIGJ5IGVzc2F5Lg0KDQohW10oaHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL2thZ2dsZS1jb21wZXRpdGlvbnMva2FnZ2xlLzM4MzIxL2xvZ29zL2hlYWRlci5wbmcpDQoNCg0KW1RoaXMgaXMgdGhlIHJ1YnJpY10oaHR0cHM6Ly9kb2NzLmdvb2dsZS5jb20vZG9jdW1lbnQvZC8xUEJOc2hDQ2JqSUY3SHc0TC1kd1dIS29zTlZBSFM4UDN2SFlNX0VrcEN2QS9lZGl0KSB0aGF0IHdhcyB1c2VkIHRvIGdyYWRlIHRoZSBlc3NheXMuIFR3byBwZW9wbGUgZGlkIHRoZSB3b3JrIGluZGVwZW5kZW50bHksIGFuZCB0aGVuIHRoZSBzY29yZXMgd2VyZSBjb21wYXJlZCBmb3IgYWxpZ25tZW50Lg0KDQojIyBQcmVwcm9jZXNzaW5nIA0KDQpOYXR1cmFsIExhbmd1YWdlIFByb2Nlc3NpbmcgdGVjaG5pcXVlcyBvZmZlciBhIHdpZGUgdmFyaWV0eSBvZiB0b29scyB0byBhcHByb2FjaCB0aGlzIHByb2JsZW0uIFRoZSBLYWdnbGUgaG9zdCBpcyByZXF1aXJpbmcgdGhhdCB0aGUgbW9kZWwgcnVuIGFzIGEgc3RhbmRhbG9uZSwgd2l0aG91dCBpbnRlcm5ldCBhc3Npc3RhbmNlLiBUaGV5IGFsc28gYXNrIGZvciBhIHBhcnNpbW9ub3VzLCBleHBsYWluYWJsZSBtb2RlbC4NCg0KV2Ugd2lsbCBzdGFydCB3aXRoIGV4cGxvcmluZyB0aGUgcHJlZGljdGl2ZSBwb3RlbnRpYWwgb2YgdGhlIHRleHQgY291bnQgZmVhdHVyZXMsIGxpa2UgbnVtYmVycyBvZiB3b3JkcywgZGlzdGluY3Qgd29yZHMsIGFuZCBzcGFjZXMuDQoNClVuc3VwZXJ2aXNlZCB0b3BpYyBncm91cGluZyBjYXRlZ29yaWVzIG1heSBiZSB1c2VmdWwgZm9yIG1lYXN1cmVzIGxpa2UgY29udmVudGlvbnMgb3IgZ3JhbW1hci4gIEluIHRoaXMgY2FzZSwgd2Ugd2lsbCBzdGFydCB3aXRoIFtMYXRlbnQgRGlyaWNobGV0IGFsbG9jYXRpb25dKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0xhdGVudF9EaXJpY2hsZXRfYWxsb2NhdGlvbikgKExEQSkuDQoNCkluZGl2aWR1YWwgd29yZHMgbWF5IGhhdmUgcHJlZGljdGl2ZSBwb3dlciwgYnV0IHRoZXkgY291bGQgYmUgc28gc3BhcnNlIGFzIHRvIGJlIGRpZmZpY3VsdCB0byBzZXBhcmF0ZSBmcm9tIHRoZSBiYWNrZ3JvdW5kIG5vaXNlLiBDb25zaWRlciB3b3JkcyBsaWtlICphaW4ndCogYW5kIHBocmFzZXMgdGFrZW4gZnJvbSBvdGhlciBsYW5ndWFnZXMuDQoNCkJyaW5naW5nIGluIGEgc2VudGltZW50IGRpY3Rpb25hcnkgbWF5IGFkZCBwcmVkaWN0aXZlIHBvd2VyIHRvIHNvbWUgbWVhc3VyZXMsIGFsb25nIHdpdGggaGVscGluZyB0byBjb3VudCBtaXNzLXNwZWxsaW5ncy4gV29yZCBlbWJlZGRpbmdzIGxpa2UgR2xvdmUgb3IgSHVnZ2luZ2ZhY2UgY291bGQgYWxzbyBiZXR0ZXIgY2hhcmFjdGVyaXplIG1lYW5pbmcuIA0KDQojIyBNb2RlbGluZyANCg0KTWFueSBkZXZlbG9wZXJzIGFyZSB0ZW1wdGVkIHRvIGp1bXAgaW50byAoQ05OIC8gTFNUTSkgZGVlcCBsZWFybmluZywgYnV0IHRoZSBudW1iZXIgb2YgZXNzYXlzIGlzIHJlYWxseSBwcmV0dHkgc21hbGwgZm9yIGEgZGVlcCBsZWFybmluZyBydW4gb24gdGhlaXIgb3duLiBBbm90aGVyIGFwcHJvYWNoIGNvdWxkIGxldmVyYWdlIHRoZSBwcmUtdHJhaW5lZCBlbWJlZGRpbmdzIGluIG9uZSBvZiB0aGUgW0JFUlRdKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL0JFUlRfKGxhbmd1YWdlX21vZGVsKSlzLiBUaGUgY3VycmVudCBLYWdnbGUgbGVhZGVyYm9hcmQgaXMgZnVsbCBvZiB0aGVtLiBFdmVuIHNvLCB0aGUgc3RhbmRpbmdzIHdpbGwgc2hpZnQgaW4gYSBodWdlIHdheSBhZnRlciB0aGUgZnVsbCB0ZXN0IHNldCBjYWxjdWxhdGlvbnMgYXBwZWFyIGJlY2F1c2Ugb2Ygb3ZlcmZpdHRpbmcgYW5kIGltYmFsYW5jZS4NCg0KVGhlIEdsb1ZlIHByZS10cmFpbmVkIHdvcmQgdmVjdG9ycyBwcm92aWRlIHdvcmQgZW1iZWRkaW5ncyBjcmVhdGVkIG9uIGV4aXN0aW5nIGRvY3VtZW50IGNvcnB1cywgYW5kIGFyZSBwcm92aWRlZCBhcyBhIHByZS1wcm9jZXNzb3IgdXNpbmcgdmFyeWluZyBudW1iZXJzIG9mIHRva2Vucy4gU2VlIFtKZWZmcmV5IFBlbm5pbmd0b24sIFJpY2hhcmQgU29jaGVyLCBhbmQgQ2hyaXN0b3BoZXIgRC4gTWFubmluZy4gMjAxNC4gR2xvVmU6IEdsb2JhbCBWZWN0b3JzIGZvciBXb3JkIFJlcHJlc2VudGF0aW9uLl0oaHR0cHM6Ly9ubHAuc3RhbmZvcmQuZWR1L3Byb2plY3RzL2dsb3ZlLykgZm9yIGRldGFpbHMuDQoNCkkgc3BlbnQgYSBmZXcgZXZlbmluZ3Mgd2l0aCB0aGUgdG9yY2gvYGJydWxlZWAgYXBwcm9hY2ggb24gYHRpZHltb2RlbHNgLCBidXQgZGlzY292ZXJlZCB0aGF0IG1vZGVsaW5nIHRpbWUgY29uc3VtZWQgd291bGQgYmUgc2lnbmlmaWNhbnQgYW5kIHRoZSByZXN1bHRzIHdlcmUgbm90IGJldHRlciB0aGFuIHJhbmRvbSBmb3Jlc3RzIG9uIGVuZ2luZWVyZWQgZmVhdHVyZXMgd2l0aCBjYXNlIHdlaWdodHMgYmFzZWQgb24gaW52ZXJzZSBwcm9wb3J0aW9ucyBvZiB0aGUgbWV0cmljIHZhbHVlcy4NCg0KSSB1bHRpbWF0ZWx5IHNldHRsZWQgb24gdGhlIGB4Z2Jvb3N0YCBhcHByb2FjaCBoZXJlLiBObyBkb3VidCBpdCBjYW4gc3RpbGwgb3ZlcmZpdCBvbiBzcGVjaWZpYyB3b3JkcyBhbmQgdGV4dCBhdHRyaWJ1dGVzLCBsaWtlIHRoZSBudW1iZXIgb2YgdW5pcXVlIHdvcmRzLiANCg0KT25lIGxhc3QgcG9pbnQuIEkgYmVsaWV2ZSB0aGF0IHRoZSBFc3NheSBTY29yaW5nIGlzIGRvbmUgYnkgaHVtYW5zIGluIGEgd2F5IHdoZXJlIHRoZSBtZXRyaWNzIGFyZSBqdWRnZWQgdG9nZXRoZXIsIGFuZCBub3QgZW50aXJlbHkgaW5kZXBlbmRlbnRseS4gSW4gb3RoZXIgd29yZHMsIGxvdyBgZ3JhbW1hcmAgYW5kIGxvdyBgY29oZXNpb25gIGFyZSByZWxhdGVkLg0KDQpgYGB7cn0NCiN8IGxhYmVsOiBwdWxsIHBhY2thZ2VzIGludG8gbWVtb3J5DQoNCnN1cHByZXNzUGFja2FnZVN0YXJ0dXBNZXNzYWdlcyh7DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCiAgDQpsaWJyYXJ5KHRpZHltb2RlbHMpDQpsaWJyYXJ5KHRleHQydmVjKSAjIGZvciB0b3BpYyBtb2RlbGluZw0KDQpsaWJyYXJ5KHRpZHl0ZXh0KQ0KbGlicmFyeSh0ZXh0cmVjaXBlcykNCg0KfSkNCg0KdGlkeW1vZGVsczo6dGlkeW1vZGVsc19wcmVmZXIoKQ0KDQp0aGVtZV9zZXQodGhlbWVfbWluaW1hbCgpKQ0KDQpgYGANCg0KTGV0J3MgcmVhZCB0aGUgZGF0YSBmcm9tIEthZ2dsZSdzIGNzdidzIGludG8gZGF0YWZyYW1lcy4gDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IHJlYWQgZGF0YSBmaWxlcywgYWRkIHByZQ0KDQp0cmFpbl9lc3NheXNfcmF3IDwtIHJlYWRfY3N2KGhlcmU6OmhlcmUoImRhdGEiLCJ0cmFpbi5jc3YiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBzaG93X2NvbF90eXBlcyA9IEZBTFNFKSANCg0Kc3VibWl0X2Vzc2F5c19yYXcgPC0gcmVhZF9jc3YoaGVyZTo6aGVyZSgiZGF0YSIsInRlc3QuY3N2IiksDQogICAgICAgICAgICAgICAgICAgICAgICAgIHNob3dfY29sX3R5cGVzID0gRkFMU0UpIA0KDQpvdXRjb21lcyA9IG5hbWVzKHRyYWluX2Vzc2F5c19yYXcpWzM6OF0NCg0KZGltKHRyYWluX2Vzc2F5c19yYXcpDQoNCmBgYA0KDQpUaGUgZXNzYXkgbWV0cmljcyBzY29yZSBkaXN0cmlidXRpb25zIHJlc2VtYmxlIG9yZGluYWwgTGlrZXJ0IHNjYWxlcy4gT25lIHdheSB0byBpbGx1c3RyYXRlIHRoZSBjb3VudHMgYXQgZWFjaCBsZXZlbCBpcyB0aGlzIGJhciBjaGFydDoNCg0KYGBge3J9DQojfCBsYWJlbDogbGlrZXJ0DQoNCnN0YWdlMSA8LSB0cmFpbl9lc3NheXNfcmF3IHw+DQogIHNlbGVjdChjb2hlc2lvbjpjb252ZW50aW9ucykgfD4NCiAgcGl2b3RfbG9uZ2VyKGNvbHMgPSBldmVyeXRoaW5nKCksDQogICAgICAgICAgICAgICBuYW1lc190byA9ICJtZXRyaWMiLA0KICAgICAgICAgICAgICAgdmFsdWVzX3RvID0gImFucyIpIHw+DQogIGdyb3VwX2J5KGFucywgbWV0cmljKSB8Pg0KICBzdW1tYXJpemUobiA9IG4oKSwNCiAgICAgICAgICAgIC5ncm91cHMgPSAiZHJvcCIpIHw+DQogIGdyb3VwX2J5KG1ldHJpYykgfD4NCiAgbXV0YXRlKHBlciA9IG4gLyBzdW0obikpIHw+DQogIG11dGF0ZSgNCiAgICB0ZXh0ID0gcGFzdGUwKGZvcm1hdEMoDQogICAgICAxMDAgKiBwZXIsIGZvcm1hdCA9ICJmIiwgZGlnaXRzID0gMA0KICAgICksICIlIiksDQogICAgY3MgPSBjdW1zdW0ocGVyKSwNCiAgICBvZmZzZXQgPSBzdW0ocGVyWzE6KGZsb29yKG4oKSAvIDIpKV0pICsgKG4oKSAlJSAyKSAqIDAuNSAqIChwZXJbY2VpbGluZyhuKCkgLw0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgMildKSwNCiAgICB4bWF4ID0gLW9mZnNldCArIGNzLA0KICAgIHhtaW4gPSB4bWF4IC0gcGVyDQogICkgfD4NCiAgdW5ncm91cCgpDQoNCmdhcCA8LSAwLjINCg0Kc3RhZ2UyIDwtIHN0YWdlMSAlPiUNCiAgbGVmdF9qb2luKA0KICAgIHN0YWdlMSAlPiUNCiAgICAgIGdyb3VwX2J5KG1ldHJpYykgJT4lDQogICAgICBzdW1tYXJpemUobWF4LnhtYXggPSBtYXgoeG1heCkpICU+JQ0KICAgICAgbXV0YXRlKHIgPSByb3dfbnVtYmVyKG1heC54bWF4KSksDQogICAgYnkgPSAibWV0cmljIg0KICApICU+JQ0KICBhcnJhbmdlKGRlc2MocikpICU+JQ0KICBtdXRhdGUoeW1pbiA9IHIgLSAoMSAtIGdhcCkgLyAyLA0KICAgICAgICAgeW1heCA9IHIgKyAoMSAtIGdhcCkgLyAyKQ0KDQpnZ3Bsb3Qoc3RhZ2UyKSArDQogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDApICsNCiAgZ2VvbV9yZWN0KGFlcygNCiAgICB4bWluID0geG1pbiwNCiAgICB4bWF4ID0geG1heCwNCiAgICB5bWluID0geW1pbiwNCiAgICB5bWF4ID0geW1heCwNCiAgICBmaWxsID0gZmFjdG9yKGFucykNCiAgKSkgKw0KICBnZW9tX3RleHQoYWVzKA0KICAgIHggPSAoeG1pbiArIHhtYXgpIC8gMiwNCiAgICB5ID0gKHltaW4gKyB5bWF4KSAvIDIsDQogICAgbGFiZWwgPSB0ZXh0DQogICksDQogIHNpemUgPSAzLA0KICBjaGVja19vdmVybGFwID0gVFJVRSkgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMoDQogICAgIiIsDQogICAgbGFiZWxzID0gcGVyY2VudCwNCiAgICBicmVha3MgPSBzZXEoLTAuNiwgMC42NSwgbGVuID0gNiksDQogICAgbGltaXRzID0gYygtMC42LCAwLjY1KQ0KICApICsgICBzY2FsZV95X2NvbnRpbnVvdXMoDQogICAgIiIsDQogICAgYnJlYWtzID0gMTpuX2Rpc3RpbmN0KHN0YWdlMiRtZXRyaWMpLA0KICAgIGxhYmVscyA9IHJldihzdGFnZTIgJT4lIGRpc3RpbmN0KG1ldHJpYykgJT4lIC4kbWV0cmljKQ0KICApICsNCiAgc2NhbGVfZmlsbF9icmV3ZXIoIlNjb3JlIiwgcGFsZXR0ZSA9ICJCckJHIikgKw0KICBsYWJzKHRpdGxlID0gIlRyYWluaW5nIHNldCBFc3NheSBSYXRpbmdzIikNCg0KYGBgDQoNCkVzc2F5cyB3aXRoIG1vcmUgd29yZHMsIG9yIG1vcmUgc2VudGVuY2VzLCBkbyBub3QgbmVjZXNzYXJpbHkgc2NvcmUgYmV0dGVyLiANCg0KYGBge3J9DQojfCBsYWJlbDogb3V0Y29tZSB2YXJpYWJsZSBkaXN0cmlidXRpb25zDQoNCnRlX2xvbmcgPC0gdHJhaW5fZXNzYXlzX3JhdyB8Pg0KICBwaXZvdF9sb25nZXIoY29scyA9IGNvaGVzaW9uOmNvbnZlbnRpb25zLA0KICAgICAgICAgICAgICAgbmFtZXNfdG8gPSAibWV0cmljIiwNCiAgICAgICAgICAgICAgIHZhbHVlc190byA9ICJ2YWx1ZSIpIHw+DQogIG11dGF0ZShtZXRyaWMgPSBhcy5mYWN0b3IobWV0cmljKSwNCiAgICAgICAgIHZhbHVlID0gYXMuZmFjdG9yKHZhbHVlKSkNCg0KdGVfbG9uZyB8PiANCiAgZ3JvdXBfYnkobl93b3JkcyA9IGdncGxvdDI6OmN1dF9pbnRlcnZhbCgNCiAgICB0b2tlbml6ZXJzOjpjb3VudF93b3JkcyhmdWxsX3RleHQpLCANCiAgICBsZW5ndGggPSAyMDApLA0KICAgIG1ldHJpYywgdmFsdWUpIHw+IA0KICBzdW1tYXJpc2UoYE51bWJlciBvZiBlc3NheXNgID0gbigpLA0KICAgICAgICAgICAgLmdyb3VwcyA9ICJkcm9wIikgfD4gDQogIGdncGxvdChhZXMobl93b3JkcywgYE51bWJlciBvZiBlc3NheXNgLCBmaWxsID0gYXMuZmFjdG9yKHZhbHVlKSkpICsNCiAgZ2VvbV9jb2woKSArDQogIHNjYWxlX3hfZGlzY3JldGUoZ3VpZGUgPSBndWlkZV9heGlzKG4uZG9kZ2UgPSAyKSkgKw0KICBmYWNldF93cmFwKHZhcnMobWV0cmljKSkgKw0KICBzY2FsZV9maWxsX2JyZXdlcigiU2NvcmUiLCBwYWxldHRlID0gIkJyQkciKSArDQogIGxhYnMoeCA9ICJOdW1iZXIgb2Ygd29yZHMgcGVyIGVzc2F5IiwNCiAgICAgICB5ID0gIk51bWJlciBvZiBlc3NheXMiLA0KICAgICAgIGZpbGwgPSAiU2NvcmUiKQ0KDQp0ZV9sb25nIHw+IA0KICBncm91cF9ieShuX3dvcmRzID0gZ2dwbG90Mjo6Y3V0X2ludGVydmFsKA0KICAgIHRva2VuaXplcnM6OmNvdW50X3NlbnRlbmNlcyhmdWxsX3RleHQpLCBsZW5ndGggPSAyMCksDQogICAgbWV0cmljLCB2YWx1ZSkgfD4gDQogIHN1bW1hcmlzZShgTnVtYmVyIG9mIGVzc2F5c2AgPSBuKCksDQogICAgICAgICAgICAuZ3JvdXBzID0gImRyb3AiKSB8PiANCiAgZ2dwbG90KGFlcyhuX3dvcmRzLCBgTnVtYmVyIG9mIGVzc2F5c2AsIGZpbGwgPSBhcy5mYWN0b3IodmFsdWUpKSkgKw0KICBnZW9tX2NvbCgpICsNCiAgc2NhbGVfeF9kaXNjcmV0ZShndWlkZSA9IGd1aWRlX2F4aXMobi5kb2RnZSA9IDIpKSArDQogIGZhY2V0X3dyYXAodmFycyhtZXRyaWMpKSArDQogIHNjYWxlX2ZpbGxfYnJld2VyKCJTY29yZSIsIHBhbGV0dGUgPSAiQnJCRyIpICsNCiAgbGFicyh4ID0gIk51bWJlciBvZiBzZW50ZW5jZXMgcGVyIGVzc2F5IiwNCiAgICAgICB5ID0gIk51bWJlciBvZiBlc3NheXMiLA0KICAgICAgIGZpbGwgPSAiU2NvcmUiKQ0KDQpgYGANCg0KV2hhdCB3b3JkcyBmcm9tIHRoZSBkaWFsb2d1ZSBoYXZlIHRoZSBoaWdoZXN0IGxvZyBvZGRzIG9mIGNvbWluZyBmcm9tIGVhY2ggbGV2ZWwgb2YgZWFjaCBvdXRjb21lPyAgRG8gdGhlIGluZGl2aWR1YWwgd29yZHMgaGF2ZSBwcmVkaWN0aXZlIHBvd2VyPw0KDQpgYGB7cn0NCiN8IGxhYmVsOiBsb2cgb2Rkcw0KDQpwbG90X2xvZ19vZGRzIDwtIGZ1bmN0aW9uKG91dGNvbWUgPSAiY29oZXNpb24iKXsNCg0KdHJhaW5fZXNzYXlzX3JhdyB8Pg0KICB0aWR5dGV4dDo6dW5uZXN0X3Rva2Vucyh3b3JkLCBmdWxsX3RleHQpIHw+IA0KICBjb3VudChsZXZlbCA9IGZhY3RvciguZGF0YVtbb3V0Y29tZV1dKSwgd29yZCwgc29ydCA9IFRSVUUpIHw+ICAgDQogIHRpZHlsbzo6YmluZF9sb2dfb2RkcyhsZXZlbCwgd29yZCwgbikgfD4gDQogIGZpbHRlcihuID4gMjApIHw+IA0KICBncm91cF9ieShsZXZlbCkgfD4gDQogIHNsaWNlX21heChsb2dfb2Rkc193ZWlnaHRlZCwgbiA9IDEwKSB8PiANCiAgbXV0YXRlKHdvcmQgPSByZW9yZGVyX3dpdGhpbih3b3JkLCBsb2dfb2Rkc193ZWlnaHRlZCwgbGV2ZWwpKSAlPiUNCiAgZ2dwbG90KGFlcyhsb2dfb2Rkc193ZWlnaHRlZCwgd29yZCwgZmlsbCA9IGxldmVsKSkgKw0KICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogIGZhY2V0X3dyYXAodmFycyhsZXZlbCksIHNjYWxlcyA9ICJmcmVlIikgKw0KICBzY2FsZV9maWxsX2JyZXdlcigiU2NvcmUiLCBwYWxldHRlID0gIkJyQkciKSArDQogIHNjYWxlX3lfcmVvcmRlcmVkKCkgKw0KICBsYWJzKHkgPSBOVUxMLCB0aXRsZSA9IGdsdWU6OmdsdWUoIntvdXRjb21lfSBsb2cgb2RkcyB3b3JkcyIpKSAgDQogICAgDQp9DQoNCm1hcChvdXRjb21lcywgcGxvdF9sb2dfb2RkcykNCg0KYGBgDQoNClRvIHNvbWUgZXh0ZW50LCB0aGUgYW5zd2VyIG1heSBiZSB5ZXMuDQoNCkxldCdzIGFsc28gdGFrZSBhIGxvb2sgYXQgb3V0Y29tZSBwYWlyd2lzZSBjb3JyZWxhdGlvbnMuDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IHByZWRpY3RvciBhbmQgb3V0Y29tZSBwYWlyd2lzZSBjb3JyZWxhdGlvbnMNCg0KdHJhaW5fZXNzYXlzX3JhdyB8PiANCiAgY29ycnI6OmNvcnJlbGF0ZSgNCiAgICBxdWlldCA9IFRSVUUNCiAgKSAlPiUNCiAgY29ycnI6OnJlYXJyYW5nZSgpICU+JQ0KICBjb3Jycjo6c2hhdmUoKSAlPiUNCiAgY29ycnI6OnJwbG90KHByaW50X2NvciA9IFRSVUUsDQogICAgICAgICAgICAgICBjb2xvcnMgPSBicmV3ZXJfcGFsKHBhbGV0dGUgPSAiQnJCRyIpKDUpKSArDQogIHNjYWxlX3hfZGlzY3JldGUoZ3VpZGUgPSBndWlkZV9heGlzKG4uZG9kZ2UgPSAzKSkNCg0KYGBgDQoNCiogICBWb2NhYnVsYXJ5IGFuZCBQaHJhc2VvbG9neSAoMC43NCkgdHJhY2sgdG9nZXRoZXIuDQoqICAgUGhyYXNlb2xvZ3kgYW5kIFN5bnRheCAoMC43MykgdHJhY2sgdG9nZXRoZXIuDQoqICAgUHJhc2VvbG9neSBhbmQgR3JhbW1hciAoMC43MikgdHJhY2sgdG9nZXRoZXIuDQoNCkF2b2lkaW5nIG92ZXJmaXR0aW5nIHRvIHRoZSB0cmFpbmluZyBkYXRhIGlzIGNyaXRpY2FsIHRvIGFjaGlldmluZyBhIHN0cm9uZyBzY29yZS4gV2UgYXJlIGdvaW5nIHRvIHVzZSByZXNhbXBsaW5nIHRvIGhhdmUgc29tZSBpbmRpY2F0aW9uIHRoYXQgb3VyIG1vZGVsIGdlbmVyYWxpemVzIHRvIG5ldyBlc3NheXMuIENhcmUgbXVzdCBiZSBleGVyY2lzZWQgdG8gYmUgc3VyZSB0aGF0IG1lbWJlcnMgb2YgdGhlIGhvbGQgb3V0IGZvbGRzIGFyZSBub3QgYWxzbyBmb3VuZCBpbiB0aGUgdHJhaW5pbmcgZm9sZHMuDQoNCkxhdGVudCBEaXJpY2hsZXQgYWxsb2NhdGlvbiAoTERBKSBpcyBhbiB1bnN1cGVydmlzZWQgZ2VuZXJhdGl2ZSBzdGF0aXN0aWNhbCBtb2RlbCB0aGF0IGV4cGxhaW5zIGEgc2V0IG9mIG9ic2VydmF0aW9ucyB0aHJvdWdoIHVub2JzZXJ2ZWQgZ3JvdXBzLCBhbmQgdGhlIGNvbnRlbnQgb2YgZWFjaCBncm91cCBtYXkgZXhwbGFpbiB3aHkgc29tZSBwYXJ0cyBvZiB0aGUgZGF0YSBhcmUgc2ltaWxhci4NCg0KSSdkIGxpa2UgdG8gZXhwbG9yZSB0aGUgdXNlIG9mIGBpbnZlcnNlIHByb2JhYmlsaXR5IHdlaWdodHNgIGJlY2F1c2UgdGhlcmUgYXJlIHNvIGZldyBlc3NheXMgd2l0aCBzY29yZXMgYXQgdGhlIGhpZ2hlc3QgYW5kIGxvd2VzdCBsZXZlbHMuIFdoZW4gc3VydmV5IHJlc3BvbmRlbnRzIGhhdmUgZGlmZmVyZW50IHByb2JhYmlsaXRpZXMgb2Ygc2VsZWN0aW9uLCAoaW52ZXJzZSkgcHJvYmFiaWxpdHkgd2VpZ2h0cyBoZWxwIHJlZHVjZSBiaWFzIGluIHRoZSByZXN1bHRzLg0KDQpJIGFtIG1ha2luZyB1cyBvZiBtZXRhcHJvZ3JhbW1pbmcgdGVjaG5pcXVlcyB0byBwYXNzIHRleHQgdmVjdG9yIGNvbHVtbiBuYW1lcyBpbnRvIHRoZSBmb3JtdWxhIGFuZCBjYXNlIHdlaWdodHMgZnVuY3Rpb25zIHRvIHJlLXVzZSB0aGVtIGZvciBlYWNoIG1ldHJpYy4NCg0KDQpgYGB7cn0NCiN8IGxhYmVsOiBwcmVwcm9jZXNzb3JzDQoNCg0KdG9rZW5zID0gdGV4dDJ2ZWM6OndvcmRfdG9rZW5pemVyKHRvbG93ZXIodHJhaW5fZXNzYXlzX3JhdyRmdWxsX3RleHQpKQ0KDQppdCA9IHRleHQydmVjOjppdG9rZW4odG9rZW5zLCBpZHMgPSB0cmFpbl9lc3NheXNfcmF3JHRleHRfaWQsIHByb2dyZXNzYmFyID0gRkFMU0UpDQoNCnYgPSB0ZXh0MnZlYzo6Y3JlYXRlX3ZvY2FidWxhcnkoaXQpDQoNCmR0bSA9IHRleHQydmVjOjpjcmVhdGVfZHRtKGl0LCB0ZXh0MnZlYzo6dm9jYWJfdmVjdG9yaXplcih2KSwgdHlwZSA9ICJSc3BhcnNlTWF0cml4IikNCg0KbGRhX21vZGVsIDwtIHRleHQydmVjOjpMREEkbmV3KG5fdG9waWNzID0gMzApDQoNCmNhc2Vfd2VpZ2h0X2J1aWxkZXIgPC0gZnVuY3Rpb24oZGF0YSwgb3V0Y29tZSkgew0KICBkYXRhICU+JQ0KICAgIGlubmVyX2pvaW4oZGF0YSAlPiUNCiAgICAgICAgICAgICAgICAgY291bnQoLmRhdGFbW291dGNvbWVdXSwNCiAgICAgICAgICAgICAgICAgICAgICAgbmFtZSA9ICJjYXNlX3d0cyIpLA0KICAgICAgICAgICAgICAgYnkgPSBnbHVlOjpnbHVlKCJ7IG91dGNvbWUgfSIpKSAlPiUNCiAgICBtdXRhdGUoY2FzZV93dHMgPSBpbXBvcnRhbmNlX3dlaWdodHMobWF4KGNhc2Vfd3RzKSAvIGNhc2Vfd3RzKSkNCn0NCg0KcmVjaXBlX2J1aWxkZXIgPC0gZnVuY3Rpb24ob3V0Y29tZSA9ICJjb2hlc2lvbiIpIHsNCiAgcmVjIDwtIHJlY2lwZSgNCiAgICBmb3JtdWxhKGdsdWU6OmdsdWUoInsgb3V0Y29tZSB9IH4gLiIpKSwNCiAgICBkYXRhID0gdHJhaW5fZXNzYXlzX3JhdyB8Pg0KICAgICAgc2VsZWN0KHsNCiAgICAgICAgew0KICAgICAgICAgIG91dGNvbWUNCiAgICAgICAgfQ0KICAgICAgfSwgZnVsbF90ZXh0KSB8Pg0KICAgICAgY2FzZV93ZWlnaHRfYnVpbGRlcihvdXRjb21lKQ0KICApIHw+DQogICAgc3RlcF90ZXh0ZmVhdHVyZShmdWxsX3RleHQsDQogICAgICAgICAgICAgICAgICAgICBrZWVwX29yaWdpbmFsX2NvbHMgPSBUUlVFKSB8Pg0KICAgIHN0ZXBfcmVuYW1lX2F0KHN0YXJ0c193aXRoKCJ0ZXh0ZmVhdHVyZV8iKSwNCiAgICAgICAgICAgICAgICAgICBmbiA9IH4gZ3N1YigidGV4dGZlYXR1cmVfZnVsbF90ZXh0XyIsICIiLCAuKSkgJT4lDQogICAgc3RlcF90b2tlbml6ZShmdWxsX3RleHQpICU+JQ0KICAgIHN0ZXBfbGRhKGZ1bGxfdGV4dCwNCiAgICAgICAgICAgICBsZGFfbW9kZWxzID0gbGRhX21vZGVsLA0KICAgICAgICAgICAgIGtlZXBfb3JpZ2luYWxfY29scyA9IFRSVUUpICU+JQ0KICAgIHN0ZXBfd29yZF9lbWJlZGRpbmdzKA0KICAgICAgZnVsbF90ZXh0LA0KICAgICAgYWdncmVnYXRpb24gPSAic3VtIiwNCiAgICAgIGVtYmVkZGluZ3MgPSB0ZXh0ZGF0YTo6ZW1iZWRkaW5nX2dsb3ZlMjdiKGRpbWVuc2lvbnMgPSAyMDApDQogICAgKSB8Pg0KICAgIHN0ZXBfenYoYWxsX251bWVyaWNfcHJlZGljdG9ycygpKSB8Pg0KICAgIHN0ZXBfbm9ybWFsaXplKGFsbF9udW1lcmljX3ByZWRpY3RvcnMoKSkNCiAgDQogIHJldHVybihyZWMpDQogIA0KfQ0KDQoNCm11bHRpY2xhc3NfcmVjaXBlX2J1aWxkZXIgPC0gZnVuY3Rpb24ob3V0Y29tZSA9ICJjb2hlc2lvbiIpIHsNCiAgcmVjIDwtIHJlY2lwZShmb3JtdWxhKGdsdWU6OmdsdWUoInsgb3V0Y29tZSB9IH4gLiIpKSwNCiAgICAgICAgICAgICAgICBkYXRhID0gY2xhc3NpZmljYXRpb25fdHJhaW5fZGYpIHw+DQogICAgc3RlcF90ZXh0ZmVhdHVyZShmdWxsX3RleHQsDQogICAgICAgICAgICAgICAgICAgICBrZWVwX29yaWdpbmFsX2NvbHMgPSBUUlVFKSB8Pg0KICAgIHN0ZXBfcmVuYW1lX2F0KHN0YXJ0c193aXRoKCJ0ZXh0ZmVhdHVyZV8iKSwNCiAgICAgICAgICAgICAgICAgICBmbiA9IH4gZ3N1YigidGV4dGZlYXR1cmVfZnVsbF90ZXh0XyIsICIiLCAuKSkgJT4lDQogICAgc3RlcF90b2tlbml6ZShmdWxsX3RleHQpICU+JQ0KICAgIHN0ZXBfbGRhKGZ1bGxfdGV4dCwNCiAgICAgICAgICAgICBsZGFfbW9kZWxzID0gbGRhX21vZGVsLA0KICAgICAgICAgICAgIGtlZXBfb3JpZ2luYWxfY29scyA9IFRSVUUpICU+JQ0KICAgIHN0ZXBfd29yZF9lbWJlZGRpbmdzKA0KICAgICAgZnVsbF90ZXh0LA0KICAgICAgYWdncmVnYXRpb24gPSAic3VtIiwNCiAgICAgIGVtYmVkZGluZ3MgPSB0ZXh0ZGF0YTo6ZW1iZWRkaW5nX2dsb3ZlMjdiKGRpbWVuc2lvbnMgPSAyMDApDQogICAgKSB8Pg0KICAgIHN0ZXBfenYoYWxsX251bWVyaWNfcHJlZGljdG9ycygpKSB8Pg0KICAgIHN0ZXBfbm9ybWFsaXplKGFsbF9udW1lcmljX3ByZWRpY3RvcnMoKSkNCiAgDQogIHJldHVybihyZWMpDQogIA0KfQ0KDQpwbG90X3ByZWRzIDwtIGZ1bmN0aW9uKGRhdCwgb3V0Y29tZSl7DQoNCmRhdCB8PiANCiAgZ2dwbG90KGFlcyh4ID0ge3tvdXRjb21lfX0sIHkgPSAucHJlZCkpICsNCiAgZ2VvbV9wb2ludChhbHBoYSA9IDAuMTUpICsNCiAgZ2VvbV9hYmxpbmUoY29sb3IgPSAicmVkIikgKw0KICBjb29yZF9vYnNfcHJlZCgpIA0KDQp9DQoNCmBgYA0KDQpBcyBtZW50aW9uZWQgYWJvdmUsIHRoZSBtb2RlbCBzcGVjaWZpY2F0aW9uIGlzIGB4Z2Jvb3N0YCBmb3IgcmVncmVzc2lvbiB0byBwcmVkaWN0IGEgY29udGludW91cyBvdXRjb21lIHRoYXQgcmVzZW1ibGVzIG9yZGluYWwgY2xhc3Nlcy4gDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IG1vZGVsIHNwZWNpZmljYXRpb24NCg0KeGdiX3NwZWMgPC0NCiAgYm9vc3RfdHJlZSgNCiAgICBtdHJ5ID0gNTAsICAjIDc1TA0KICAgIHRyZWVzID0gMTAwMEwsDQogICAgdHJlZV9kZXB0aCA9IDksICMgNkwNCiAgICBsZWFybl9yYXRlID0gMC4wMSwgICMgb3JpZ2luYWxseSAwLjENCiAgICBtaW5fbiA9IDM5TCwgICMgMjBMDQogICAgbG9zc19yZWR1Y3Rpb24gPSAwDQogICkgfD4gDQogIHNldF9lbmdpbmUoJ3hnYm9vc3QnKSB8PiANCiAgc2V0X21vZGUoJ3JlZ3Jlc3Npb24nKQ0KDQpzdm1fc3BlYyA8LSBzdm1fbGluZWFyKCkgfD4gDQogIHNldF9lbmdpbmUoIkxpYmxpbmVhUiIpIHw+IA0KICBzZXRfbW9kZSgiY2xhc3NpZmljYXRpb24iKSAgDQogIA0KDQpgYGANCg0KVG8gc3BlZWQgdGhlIGNvbXB1dGF0aW9ucyBsZXQncyBlbmFibGUgYSBwYXJhbGxlbCBiYWNrZW5kLiAgDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IHBhcmFsbGVsIGFuZCB0dW5pbmcgc2V0dXANCg0KYWxsX2NvcmVzIDwtIHBhcmFsbGVsbHk6OmF2YWlsYWJsZUNvcmVzKG9taXQgPSAxKQ0KYWxsX2NvcmVzDQojICANCmZ1dHVyZTo6cGxhbigibXVsdGlzZXNzaW9uIiwgd29ya2VycyA9IGFsbF9jb3JlcykgIyBvbiBXaW5kb3dzDQoNCmBgYA0KDQojIyBNb2RlbGluZyB7LnRhYnNldH0NCg0KIyMjIENvaGVzaW9uDQoNCldlIGZpdCBmb3IgYGNvaGVzaW9uYCBmaXJzdCB1c2luZyBhbiB4Z2Jvb3N0IHJlZ3Jlc3Npb24sIHVzaW5nIGNhc2Ugd2VpZ2h0cyB0byBhZGp1c3QgZm9yIHRoZSBmcmVxdWVuY3kgb2Ygb2NjdXJyZW5jZSBvZiBlYWNoIHZhbHVlIG9mIGBjb2hlc2lvbmAuICANCg0KYGBge3J9DQojfCBsYWJlbDogZml0IGNvaGVzaW9uIHJlc2FtcGxlcyByZWdyZXNzaW9uDQoNCm91dGNvbWUgPC0gb3V0Y29tZXNbMV0NCg0KcmVncmVzc2lvbl90cmFpbl9kZiA8LSB0cmFpbl9lc3NheXNfcmF3ICB8PiANCiAgICAgICAgICAgICAgICBzZWxlY3QoISFvdXRjb21lLCBmdWxsX3RleHQpIHw+IA0KICAgICAgICAgICAgICAgIGNhc2Vfd2VpZ2h0X2J1aWxkZXIob3V0Y29tZSkNCg0KcmVncmVzc2lvbl93ZiA8LSB3b3JrZmxvdyhyZWNpcGVfYnVpbGRlcihvdXRjb21lID0gb3V0Y29tZSksIHhnYl9zcGVjKSB8PiANCiAgICAgICBhZGRfY2FzZV93ZWlnaHRzKGNhc2Vfd3RzKQ0KDQpmb2xkcyA8LSB2Zm9sZF9jdihyZWdyZXNzaW9uX3RyYWluX2RmLCBzdHJhdGEgPSB7e291dGNvbWV9fSkNCg0Kc2V0LnNlZWQoNDIpICANCnJzIDwtIGZpdF9yZXNhbXBsZXMoDQogIHJlZ3Jlc3Npb25fd2YsDQogIGZvbGRzLA0KICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkpDQoNCmNvbGxlY3RfbWV0cmljcyhycykgfD4gYXJyYW5nZShtZWFuKQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgcGxvdF9wcmVkcyguZGF0YVtbb3V0Y29tZV1dKSArDQogIGxhYnMoeSA9ICJQcmVkaWN0ZWQiLA0KICAgICAgIHRpdGxlID0gcGFzdGUwKG91dGNvbWUsICIgcHJlZGljdGlvbnMgYWdhaW5zdCBlc3NheXMgaW4gaGVsZCBvdXQgZm9sZHMiKSwNCiAgICAgICBzdWJ0aXRsZSA9ICJUaGUgaGlnaGVzdCBhbmQgbG93ZXN0IGVzc2F5cyBhcmUgbm90IHByZWRpY3RlZCB3ZWxsIikNCg0KdHJhaW5fZXNzYXlzX3Jhd1sNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhycykgfD4gDQogIG11dGF0ZShyZXNpZHVhbCA9IC5kYXRhW1tvdXRjb21lXV0gLSAucHJlZCkgfD4gDQogIGFycmFuZ2UoZGVzYyhhYnMocmVzaWR1YWwpKSkgfD4gDQogIHNsaWNlX2hlYWQobiA9IDUpIHw+IA0KICBwdWxsKC5yb3cpDQoNCiwgXSB8PiANCiAgc2VsZWN0KGZ1bGxfdGV4dCkNCg0KcmVncmVzc2lvbl9maXQgPC0gcGFyc25pcDo6Zml0KHJlZ3Jlc3Npb25fd2YsIA0KICAgICAgICAgICAgICAgICAgICAgcmVncmVzc2lvbl90cmFpbl9kZikNCg0KYGBgDQoNCklkZW50aWZ5aW5nIGV4YW1wbGVzIHdpdGggZXNwZWNpYWxseSBwb29yIHBlcmZvcm1hbmNlIGNhbiBoZWxwIHVzIGZvbGxvdyB1cCBhbmQgaW52ZXN0aWdhdGUgd2h5IHRoZXNlIHNwZWNpZmljIHByZWRpY3Rpb25zIGFyZSBwb29yLiBDb25jZXB0dWFsbHksIGl0cyBlYXN5IGZvciBhIGJhc2VsaW5lIGtub3ctbm90aGluZyBtb2RlbCB0byBhc3NpZ24gYWxsIGVzc2F5cyB0byB0aGUgbWVkaWFuIHNjb3JlIG9mIDMuIFRoZSBwcmVkaWN0aXZlIHBvd2VyIGlzIGluIHRoZSBhYmlsaXR5IHRvIG1vZGVsIHRoZSBlc3NheXMgdGhhdCBhcmUgbm90IDMgaW50byBidWNrZXRzIGhpZ2hlciBhbmQgbG93ZXIgdGhhbiAzLg0KDQpCZWNhdXNlIHRoZSByYXRpbmdzIGFyZSBhIGZvcm0gb2Ygb3JkaW5hbCB2YWx1ZSwgb3IgZXZlbiBhIGxpa2VydCBzY2FsZSwgd2Ugd2lsbCBlbnNlbWJsZSBhIHNlY29uZCBjbGFzc2lmaWNhdGlvbiBtb2RlbCB0aGF0IGluY2x1ZGVzIHRoZSBvdXRwdXQgb2YgdGhlIHJlZ3Jlc3Npb24uDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IGZpdCBjb2hlc2lvbiByZXNhbXBsZXMgY2xhc3NpZmljYXRpb24NCg0KY2xhc3NpZmljYXRpb25fdHJhaW5fZGYgPC0gdHJhaW5fZXNzYXlzX3JhdyAgfD4gDQogICAgICAgICAgICAgICAgc2VsZWN0KHt7b3V0Y29tZX19LCBmdWxsX3RleHQpIHw+IA0KICAgICAgICAgICAgICAgIGJpbmRfY29scygNCiAgICAgICAgICAgICAgICAgIHByZWRpY3QoDQogICAgICAgICAgICAgICAgICAgIHJlZ3Jlc3Npb25fZml0LA0KICAgICAgICAgICAgICAgICAgICByZWdyZXNzaW9uX3RyYWluX2RmDQogICAgICAgICAgICAgICAgICApDQogICAgICAgICAgICAgICAgKSB8PiANCiAgICAgICAgICAgICAgIHJlbmFtZShyZWdyZXNzaW9uX3ByZWQgPSAucHJlZCkgfD4gDQogICAgICAgICAgICAgICBtdXRhdGUoe3tvdXRjb21lfX0gOj0gZmFjdG9yKC5kYXRhW1tvdXRjb21lXV0pKQ0KDQpjbGFzc2lmaWNhdGlvbl93ZiA8LSB3b3JrZmxvdyhtdWx0aWNsYXNzX3JlY2lwZV9idWlsZGVyKG91dGNvbWUgPSBvdXRjb21lKSwgc3ZtX3NwZWMpIA0KDQpmb2xkcyA8LSB2Zm9sZF9jdihjbGFzc2lmaWNhdGlvbl90cmFpbl9kZiwgc3RyYXRhID0gISFvdXRjb21lKQ0KDQpzZXQuc2VlZCg0MikgIA0KcnMgPC0gZml0X3Jlc2FtcGxlcygNCiAgY2xhc3NpZmljYXRpb25fd2YsDQogIGZvbGRzLA0KICBtZXRyaWNzID0gbWV0cmljX3NldChrYXAsIGFjY3VyYWN5KSwNCiAgY29udHJvbCA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IFRSVUUpKQ0KDQpjb2xsZWN0X21ldHJpY3MocnMpIHw+IGFycmFuZ2UobWVhbikNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhycykgfD4gDQogIGdncGxvdChhZXMoeCA9IC5kYXRhW1tvdXRjb21lXV0sIHkgPSBhYnMoYXMubnVtZXJpYyguZGF0YVtbb3V0Y29tZV1dKSAtIGFzLm51bWVyaWMoLnByZWRfY2xhc3MpKS8yKSkgKw0KICBnZW9tX3Zpb2xpbigpICsNCiAgc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgtNSw1LDAuNSkpICsNCiAgbGFicyh5ID0gIlJlc2lkdWFscyIsDQogICAgICAgdGl0bGUgPSAie3tvdXRjb21lfX0gUmVzaWR1YWwgZXJyb3JzIGZvciBlc3NheXMgaW4gaGVsZCBvdXQgZm9sZHMiLA0KICAgICAgIHN1YnRpdGxlID0gIlRoZSBoaWdoZXN0IGFuZCBsb3dlc3QgZXNzYXlzIGFyZSBzdGlsbCBub3QgcHJlZGljdGVkIHdlbGwiKQ0KDQp0cmFpbl9lc3NheXNfcmF3Ww0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgbXV0YXRlKHJlc2lkdWFsID0gYXMubnVtZXJpYyguZGF0YVtbb3V0Y29tZV1dKSAtIGFzLm51bWVyaWMoLnByZWRfY2xhc3MpKSB8PiANCiAgYXJyYW5nZShkZXNjKGFicyhyZXNpZHVhbCkpKSB8PiANCiAgc2xpY2VfaGVhZChuID0gNSkgfD4gDQogIHB1bGwoLnJvdykNCg0KLCBdIHw+IA0KICBzZWxlY3QoZnVsbF90ZXh0LCB7e291dGNvbWV9fSkNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhycykgfD4gDQogIHJtc2UodHJ1dGggPSBhcy5udW1lcmljKC5kYXRhW1tvdXRjb21lXV0pLzIsIGVzdGltYXRlID0gYXMubnVtZXJpYygucHJlZF9jbGFzcykvMikNCg0KYGBgDQoNClJlc3VsdHMgaGVyZSBhcmVuJ3QgZ3JlYXQsIGJ1dCB0aGV5IGFyZSBtb3JlIGFyZSBsZXNzIGNvbXBldGl0aXZlIHdpdGggdGhlIGxlYWRlcmJvYXJkIGZpZ3VyZXMuDQoNClRoZSBmaW5hbCBmaXR0aW5nIGVuc2VtYmxlcyBib3RoIHRoZSByZWdyZXNzaW9uIGFuZCBjbGFzc2lmaWNhdGlvbiBmaXRzLCBhbmQgbWFrZXMgYSBwcmVkaWN0aW9uIG9uIHRoZSBzdWJtaXNzaW9uIGVzc2F5cy4NCg0KYGBge3J9DQojfCBsYWJlbDogZml0IGNvaGVzaW9uIGZpbmFsDQoNCmNsYXNzaWZpY2F0aW9uX2ZpdCA8LSBwYXJzbmlwOjpmaXQoY2xhc3NpZmljYXRpb25fd2YsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzaWZpY2F0aW9uX3RyYWluX2RmKQ0KDQpleHRyYWN0X2ZpdF9lbmdpbmUocmVncmVzc2lvbl9maXQpIHw+IA0KICB2aXA6OnZpcChudW1fZmVhdHVyZXMgPSAyMCkNCg0Kc3VibWlzc2lvbiA8LSBwcmVkaWN0KA0KICBjbGFzc2lmaWNhdGlvbl9maXQsDQogIA0KICBzdWJtaXRfZXNzYXlzX3JhdyB8Pg0KICAgIGJpbmRfY29scyhwcmVkaWN0KHJlZ3Jlc3Npb25fZml0LCBzdWJtaXRfZXNzYXlzX3JhdykpIHw+DQogICAgcmVuYW1lKHJlZ3Jlc3Npb25fcHJlZCA9IC5wcmVkKQ0KKSB8Pg0KICB0cmFuc211dGUoe3tvdXRjb21lfX0gOj0gLnByZWRfY2xhc3MpDQoNCnN1Ym1pc3Npb24NCg0KYGBgDQoNCiMjIyBTeW50YXgNCg0KV2UgZml0IGZvciBgc3ludGF4YCBmaXJzdCB1c2luZyBhbiB4Z2Jvb3N0IHJlZ3Jlc3Npb24sIHVzaW5nIGNhc2Ugd2VpZ2h0cyB0byBhZGp1c3QgZm9yIHRoZSBmcmVxdWVuY3kgb2Ygb2NjdXJyZW5jZSBvZiBlYWNoIHZhbHVlIG9mIGBzeW50YXhgLiAgDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IGZpdCBzeW50YXggcmVzYW1wbGVzIHJlZ3Jlc3Npb24NCg0Kb3V0Y29tZSA8LSBvdXRjb21lc1syXQ0KDQpyZWdyZXNzaW9uX3RyYWluX2RmIDwtIHRyYWluX2Vzc2F5c19yYXcgIHw+IA0KICAgICAgICAgICAgICAgIHNlbGVjdCghIW91dGNvbWUsIGZ1bGxfdGV4dCkgfD4gDQogICAgICAgICAgICAgICAgY2FzZV93ZWlnaHRfYnVpbGRlcihvdXRjb21lKQ0KDQpyZWdyZXNzaW9uX3dmIDwtIHdvcmtmbG93KHJlY2lwZV9idWlsZGVyKG91dGNvbWUgPSBvdXRjb21lKSwgeGdiX3NwZWMpIHw+IA0KICAgICAgIGFkZF9jYXNlX3dlaWdodHMoY2FzZV93dHMpDQoNCmZvbGRzIDwtIHZmb2xkX2N2KHJlZ3Jlc3Npb25fdHJhaW5fZGYsIHN0cmF0YSA9IHt7b3V0Y29tZX19KQ0KDQpzZXQuc2VlZCg0MikgIA0KcnMgPC0gZml0X3Jlc2FtcGxlcygNCiAgcmVncmVzc2lvbl93ZiwNCiAgZm9sZHMsDQogIGNvbnRyb2wgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKSkNCg0KY29sbGVjdF9tZXRyaWNzKHJzKSB8PiBhcnJhbmdlKG1lYW4pDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBwbG90X3ByZWRzKC5kYXRhW1tvdXRjb21lXV0pICsNCiAgbGFicyh5ID0gIlByZWRpY3RlZCIsDQogICAgICAgdGl0bGUgPSBwYXN0ZTAob3V0Y29tZSwgIiBwcmVkaWN0aW9ucyBhZ2FpbnN0IGVzc2F5cyBpbiBoZWxkIG91dCBmb2xkcyIpLA0KICAgICAgIHN1YnRpdGxlID0gIlRoZSBoaWdoZXN0IGFuZCBsb3dlc3QgZXNzYXlzIGFyZSBub3QgcHJlZGljdGVkIHdlbGwiKQ0KDQoNCnRyYWluX2Vzc2F5c19yYXdbDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBtdXRhdGUocmVzaWR1YWwgPSAuZGF0YVtbb3V0Y29tZV1dIC0gLnByZWQpIHw+IA0KICBhcnJhbmdlKGRlc2MoYWJzKHJlc2lkdWFsKSkpIHw+IA0KICBzbGljZV9oZWFkKG4gPSA1KSB8PiANCiAgcHVsbCgucm93KQ0KDQosIF0gfD4gDQogIHNlbGVjdChmdWxsX3RleHQpDQoNCnJlZ3Jlc3Npb25fZml0IDwtIHBhcnNuaXA6OmZpdChyZWdyZXNzaW9uX3dmLCANCiAgICAgICAgICAgICAgICAgICAgIHJlZ3Jlc3Npb25fdHJhaW5fZGYpDQoNCmBgYA0KDQpJZGVudGlmeWluZyBleGFtcGxlcyB3aXRoIGVzcGVjaWFsbHkgcG9vciBwZXJmb3JtYW5jZSBjYW4gaGVscCB1cyBmb2xsb3cgdXAgYW5kIGludmVzdGlnYXRlIHdoeSB0aGVzZSBzcGVjaWZpYyBwcmVkaWN0aW9ucyBhcmUgcG9vci4gQ29uY2VwdHVhbGx5LCBpdHMgZWFzeSBmb3IgYSBiYXNlbGluZSBrbm93LW5vdGhpbmcgbW9kZWwgdG8gYXNzaWduIGFsbCBlc3NheXMgdG8gdGhlIG1lZGlhbiBzY29yZSBvZiAzLiBUaGUgcHJlZGljdGl2ZSBwb3dlciBpcyBpbiB0aGUgYWJpbGl0eSB0byBtb2RlbCB0aGUgZXNzYXlzIHRoYXQgYXJlIG5vdCAzIGludG8gYnVja2V0cyBoaWdoZXIgYW5kIGxvd2VyIHRoYW4gMy4NCg0KQmVjYXVzZSB0aGUgcmF0aW5ncyBhcmUgYSBmb3JtIG9mIG9yZGluYWwgdmFsdWUsIG9yIGV2ZW4gYSBsaWtlcnQgc2NhbGUsIHdlIHdpbGwgZW5zZW1ibGUgYSBzZWNvbmQgY2xhc3NpZmljYXRpb24gbW9kZWwgdGhhdCBpbmNsdWRlcyB0aGUgb3V0cHV0IG9mIHRoZSByZWdyZXNzaW9uLg0KDQpgYGB7cn0NCiN8IGxhYmVsOiBmaXQgc3ludGF4IHJlc2FtcGxlcyBjbGFzc2lmaWNhdGlvbg0KDQpjbGFzc2lmaWNhdGlvbl90cmFpbl9kZiA8LSB0cmFpbl9lc3NheXNfcmF3ICB8PiANCiAgICAgICAgICAgICAgICBzZWxlY3Qoe3tvdXRjb21lfX0sIGZ1bGxfdGV4dCkgfD4gDQogICAgICAgICAgICAgICAgYmluZF9jb2xzKA0KICAgICAgICAgICAgICAgICAgcHJlZGljdCgNCiAgICAgICAgICAgICAgICAgICAgcmVncmVzc2lvbl9maXQsDQogICAgICAgICAgICAgICAgICAgIHJlZ3Jlc3Npb25fdHJhaW5fZGYNCiAgICAgICAgICAgICAgICAgICkNCiAgICAgICAgICAgICAgICApIHw+IA0KICAgICAgICAgICAgICAgcmVuYW1lKHJlZ3Jlc3Npb25fcHJlZCA9IC5wcmVkKSB8PiANCiAgICAgICAgICAgICAgIG11dGF0ZSh7e291dGNvbWV9fSA6PSBmYWN0b3IoLmRhdGFbW291dGNvbWVdXSkpDQoNCmNsYXNzaWZpY2F0aW9uX3dmIDwtIHdvcmtmbG93KG11bHRpY2xhc3NfcmVjaXBlX2J1aWxkZXIob3V0Y29tZSA9IG91dGNvbWUpLCBzdm1fc3BlYykgDQoNCmZvbGRzIDwtIHZmb2xkX2N2KGNsYXNzaWZpY2F0aW9uX3RyYWluX2RmLCBzdHJhdGEgPSAhIW91dGNvbWUpDQoNCnNldC5zZWVkKDQyKSAgDQpycyA8LSBmaXRfcmVzYW1wbGVzKA0KICBjbGFzc2lmaWNhdGlvbl93ZiwNCiAgZm9sZHMsDQogIG1ldHJpY3MgPSBtZXRyaWNfc2V0KGthcCwgYWNjdXJhY3kpLA0KICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkpDQoNCmNvbGxlY3RfbWV0cmljcyhycykgfD4gYXJyYW5nZShtZWFuKQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgZ2dwbG90KGFlcyh4ID0gLmRhdGFbW291dGNvbWVdXSwgeSA9IGFicyhhcy5udW1lcmljKC5kYXRhW1tvdXRjb21lXV0pIC0gYXMubnVtZXJpYygucHJlZF9jbGFzcykpLzIpKSArDQogIGdlb21fdmlvbGluKCkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKC01LDUsMC41KSkgKw0KICBsYWJzKHkgPSAiUmVzaWR1YWxzIiwNCiAgICAgICB0aXRsZSA9ICJ7e291dGNvbWV9fSBSZXNpZHVhbCBlcnJvcnMgZm9yIGVzc2F5cyBpbiBoZWxkIG91dCBmb2xkcyIsDQogICAgICAgc3VidGl0bGUgPSAiVGhlIGhpZ2hlc3QgYW5kIGxvd2VzdCBlc3NheXMgYXJlIHN0aWxsIG5vdCBwcmVkaWN0ZWQgd2VsbCIpDQoNCnRyYWluX2Vzc2F5c19yYXdbDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBtdXRhdGUocmVzaWR1YWwgPSBhcy5udW1lcmljKC5kYXRhW1tvdXRjb21lXV0pIC0gYXMubnVtZXJpYygucHJlZF9jbGFzcykpIHw+IA0KICBhcnJhbmdlKGRlc2MoYWJzKHJlc2lkdWFsKSkpIHw+IA0KICBzbGljZV9oZWFkKG4gPSA1KSB8PiANCiAgcHVsbCgucm93KQ0KDQosIF0gfD4gDQogIHNlbGVjdChmdWxsX3RleHQsIHt7b3V0Y29tZX19KQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgcm1zZSh0cnV0aCA9IGFzLm51bWVyaWMoLmRhdGFbW291dGNvbWVdXSkvMiwgZXN0aW1hdGUgPSBhcy5udW1lcmljKC5wcmVkX2NsYXNzKS8yKQ0KDQoNCmBgYA0KDQpSZXN1bHRzIGhlcmUgYXJlbid0IGdyZWF0LCBidXQgdGhleSBhcmUgbW9yZSBhcmUgbGVzcyBjb21wZXRpdGl2ZSB3aXRoIHRoZSBsZWFkZXJib2FyZCBmaWd1cmVzLg0KDQpUaGUgZmluYWwgZml0dGluZyBlbnNlbWJsZXMgYm90aCB0aGUgcmVncmVzc2lvbiBhbmQgY2xhc3NpZmljYXRpb24gZml0cywgYW5kIG1ha2VzIGEgcHJlZGljdGlvbiBvbiB0aGUgc3VibWlzc2lvbiBlc3NheXMuDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IGZpdCBzeW50YXggZmluYWwNCg0KY2xhc3NpZmljYXRpb25fZml0IDwtIHBhcnNuaXA6OmZpdChjbGFzc2lmaWNhdGlvbl93ZiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NpZmljYXRpb25fdHJhaW5fZGYpDQoNCmV4dHJhY3RfZml0X2VuZ2luZShyZWdyZXNzaW9uX2ZpdCkgfD4gDQogIHZpcDo6dmlwKG51bV9mZWF0dXJlcyA9IDIwKQ0KDQpzdWJtaXNzaW9uIDwtIHByZWRpY3QoDQogIGNsYXNzaWZpY2F0aW9uX2ZpdCwNCiAgDQogIHN1Ym1pdF9lc3NheXNfcmF3IHw+DQogICAgYmluZF9jb2xzKHByZWRpY3QocmVncmVzc2lvbl9maXQsIHN1Ym1pdF9lc3NheXNfcmF3KSkgfD4NCiAgICByZW5hbWUocmVncmVzc2lvbl9wcmVkID0gLnByZWQpDQopIHw+DQogIHRyYW5zbXV0ZSh7e291dGNvbWV9fSA6PSAucHJlZF9jbGFzcykgfD4NCiAgYmluZF9jb2xzKHN1Ym1pc3Npb24pDQoNCnN1Ym1pc3Npb24NCg0KYGBgDQoNCiMjIyBWb2NhYnVsYXJ5DQoNCldlIGZpdCBmb3IgYHZvY2FidWxhcnlgIGZpcnN0IHVzaW5nIGFuIHhnYm9vc3QgcmVncmVzc2lvbiwgdXNpbmcgY2FzZSB3ZWlnaHRzIHRvIGFkanVzdCBmb3IgdGhlIGZyZXF1ZW5jeSBvZiBvY2N1cnJlbmNlIG9mIGVhY2ggdmFsdWUgb2YgYHZvY2FidWxhcnlgLiAgDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IGZpdCB2b2NhYnVsYXJ5IHJlc2FtcGxlcyByZWdyZXNzaW9uDQoNCm91dGNvbWUgPC0gb3V0Y29tZXNbM10NCg0KcmVncmVzc2lvbl90cmFpbl9kZiA8LSB0cmFpbl9lc3NheXNfcmF3ICB8PiANCiAgICAgICAgICAgICAgICBzZWxlY3QoISFvdXRjb21lLCBmdWxsX3RleHQpIHw+IA0KICAgICAgICAgICAgICAgIGNhc2Vfd2VpZ2h0X2J1aWxkZXIob3V0Y29tZSkNCg0KcmVncmVzc2lvbl93ZiA8LSB3b3JrZmxvdyhyZWNpcGVfYnVpbGRlcihvdXRjb21lID0gb3V0Y29tZSksIHhnYl9zcGVjKSB8PiANCiAgICAgICBhZGRfY2FzZV93ZWlnaHRzKGNhc2Vfd3RzKQ0KDQpmb2xkcyA8LSB2Zm9sZF9jdihyZWdyZXNzaW9uX3RyYWluX2RmLCBzdHJhdGEgPSB7e291dGNvbWV9fSkNCg0Kc2V0LnNlZWQoNDIpICANCnJzIDwtIGZpdF9yZXNhbXBsZXMoDQogIHJlZ3Jlc3Npb25fd2YsDQogIGZvbGRzLA0KICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkpDQoNCmNvbGxlY3RfbWV0cmljcyhycykgfD4gYXJyYW5nZShtZWFuKQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgcGxvdF9wcmVkcyguZGF0YVtbb3V0Y29tZV1dKSArDQogIGxhYnMoeSA9ICJQcmVkaWN0ZWQiLA0KICAgICAgIHRpdGxlID0gcGFzdGUwKG91dGNvbWUsICIgcHJlZGljdGlvbnMgYWdhaW5zdCBlc3NheXMgaW4gaGVsZCBvdXQgZm9sZHMiKSwNCiAgICAgICBzdWJ0aXRsZSA9ICJUaGUgaGlnaGVzdCBhbmQgbG93ZXN0IGVzc2F5cyBhcmUgbm90IHByZWRpY3RlZCB3ZWxsIikNCg0KDQp0cmFpbl9lc3NheXNfcmF3Ww0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgbXV0YXRlKHJlc2lkdWFsID0gLmRhdGFbW291dGNvbWVdXSAtIC5wcmVkKSB8PiANCiAgYXJyYW5nZShkZXNjKGFicyhyZXNpZHVhbCkpKSB8PiANCiAgc2xpY2VfaGVhZChuID0gNSkgfD4gDQogIHB1bGwoLnJvdykNCg0KLCBdIHw+IA0KICBzZWxlY3QoZnVsbF90ZXh0KQ0KDQpyZWdyZXNzaW9uX2ZpdCA8LSBwYXJzbmlwOjpmaXQocmVncmVzc2lvbl93ZiwgDQogICAgICAgICAgICAgICAgICAgICByZWdyZXNzaW9uX3RyYWluX2RmKQ0KDQpgYGANCg0KSWRlbnRpZnlpbmcgZXhhbXBsZXMgd2l0aCBlc3BlY2lhbGx5IHBvb3IgcGVyZm9ybWFuY2UgY2FuIGhlbHAgdXMgZm9sbG93IHVwIGFuZCBpbnZlc3RpZ2F0ZSB3aHkgdGhlc2Ugc3BlY2lmaWMgcHJlZGljdGlvbnMgYXJlIHBvb3IuIENvbmNlcHR1YWxseSwgaXRzIGVhc3kgZm9yIGEgYmFzZWxpbmUga25vdy1ub3RoaW5nIG1vZGVsIHRvIGFzc2lnbiBhbGwgZXNzYXlzIHRvIHRoZSBtZWRpYW4gc2NvcmUgb2YgMy4gVGhlIHByZWRpY3RpdmUgcG93ZXIgaXMgaW4gdGhlIGFiaWxpdHkgdG8gbW9kZWwgdGhlIGVzc2F5cyB0aGF0IGFyZSBub3QgMyBpbnRvIGJ1Y2tldHMgaGlnaGVyIGFuZCBsb3dlciB0aGFuIDMuDQoNCkJlY2F1c2UgdGhlIHJhdGluZ3MgYXJlIGEgZm9ybSBvZiBvcmRpbmFsIHZhbHVlLCBvciBldmVuIGEgbGlrZXJ0IHNjYWxlLCB3ZSB3aWxsIGVuc2VtYmxlIGEgc2Vjb25kIGNsYXNzaWZpY2F0aW9uIG1vZGVsIHRoYXQgaW5jbHVkZXMgdGhlIG91dHB1dCBvZiB0aGUgcmVncmVzc2lvbi4NCg0KYGBge3J9DQojfCBsYWJlbDogZml0IHZvY2FidWxhcnkgcmVzYW1wbGVzIGNsYXNzaWZpY2F0aW9uDQoNCmNsYXNzaWZpY2F0aW9uX3RyYWluX2RmIDwtIHRyYWluX2Vzc2F5c19yYXcgIHw+IA0KICAgICAgICAgICAgICAgIHNlbGVjdCh7e291dGNvbWV9fSwgZnVsbF90ZXh0KSB8PiANCiAgICAgICAgICAgICAgICBiaW5kX2NvbHMoDQogICAgICAgICAgICAgICAgICBwcmVkaWN0KA0KICAgICAgICAgICAgICAgICAgICByZWdyZXNzaW9uX2ZpdCwNCiAgICAgICAgICAgICAgICAgICAgcmVncmVzc2lvbl90cmFpbl9kZg0KICAgICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgICAgICkgfD4gDQogICAgICAgICAgICAgICByZW5hbWUocmVncmVzc2lvbl9wcmVkID0gLnByZWQpIHw+IA0KICAgICAgICAgICAgICAgbXV0YXRlKHt7b3V0Y29tZX19IDo9IGZhY3RvciguZGF0YVtbb3V0Y29tZV1dKSkNCg0KY2xhc3NpZmljYXRpb25fd2YgPC0gd29ya2Zsb3cobXVsdGljbGFzc19yZWNpcGVfYnVpbGRlcihvdXRjb21lID0gb3V0Y29tZSksIHN2bV9zcGVjKSANCg0KZm9sZHMgPC0gdmZvbGRfY3YoY2xhc3NpZmljYXRpb25fdHJhaW5fZGYsIHN0cmF0YSA9ICEhb3V0Y29tZSkNCg0Kc2V0LnNlZWQoNDIpICANCnJzIDwtIGZpdF9yZXNhbXBsZXMoDQogIGNsYXNzaWZpY2F0aW9uX3dmLA0KICBmb2xkcywNCiAgbWV0cmljcyA9IG1ldHJpY19zZXQoa2FwLCBhY2N1cmFjeSksDQogIGNvbnRyb2wgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKSkNCg0KY29sbGVjdF9tZXRyaWNzKHJzKSB8PiBhcnJhbmdlKG1lYW4pDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBnZ3Bsb3QoYWVzKHggPSAuZGF0YVtbb3V0Y29tZV1dLCB5ID0gYWJzKGFzLm51bWVyaWMoLmRhdGFbW291dGNvbWVdXSkgLSBhcy5udW1lcmljKC5wcmVkX2NsYXNzKSkvMikpICsNCiAgZ2VvbV92aW9saW4oKSArDQogIHNjYWxlX3lfY29udGludW91cyhicmVha3MgPSBzZXEoLTUsNSwwLjUpKSArDQogIGxhYnMoeSA9ICJSZXNpZHVhbHMiLA0KICAgICAgIHRpdGxlID0gInt7b3V0Y29tZX19IFJlc2lkdWFsIGVycm9ycyBmb3IgZXNzYXlzIGluIGhlbGQgb3V0IGZvbGRzIiwNCiAgICAgICBzdWJ0aXRsZSA9ICJUaGUgaGlnaGVzdCBhbmQgbG93ZXN0IGVzc2F5cyBhcmUgc3RpbGwgbm90IHByZWRpY3RlZCB3ZWxsIikNCg0KdHJhaW5fZXNzYXlzX3Jhd1sNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhycykgfD4gDQogIG11dGF0ZShyZXNpZHVhbCA9IGFzLm51bWVyaWMoLmRhdGFbW291dGNvbWVdXSkgLSBhcy5udW1lcmljKC5wcmVkX2NsYXNzKSkgfD4gDQogIGFycmFuZ2UoZGVzYyhhYnMocmVzaWR1YWwpKSkgfD4gDQogIHNsaWNlX2hlYWQobiA9IDUpIHw+IA0KICBwdWxsKC5yb3cpDQoNCiwgXSB8PiANCiAgc2VsZWN0KGZ1bGxfdGV4dCwge3tvdXRjb21lfX0pDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBybXNlKHRydXRoID0gYXMubnVtZXJpYyguZGF0YVtbb3V0Y29tZV1dKS8yLCBlc3RpbWF0ZSA9IGFzLm51bWVyaWMoLnByZWRfY2xhc3MpLzIpDQoNCg0KYGBgDQoNClJlc3VsdHMgaGVyZSBhcmVuJ3QgZ3JlYXQsIGJ1dCB0aGV5IGFyZSBtb3JlIGFyZSBsZXNzIGNvbXBldGl0aXZlIHdpdGggdGhlIGxlYWRlcmJvYXJkIGZpZ3VyZXMuDQoNClRoZSBmaW5hbCBmaXR0aW5nIGVuc2VtYmxlcyBib3RoIHRoZSByZWdyZXNzaW9uIGFuZCBjbGFzc2lmaWNhdGlvbiBmaXRzLCBhbmQgbWFrZXMgYSBwcmVkaWN0aW9uIG9uIHRoZSBzdWJtaXNzaW9uIGVzc2F5cy4NCg0KYGBge3J9DQojfCBsYWJlbDogZml0IHZvY3VhYnVsYXJ5IGZpbmFsDQoNCmNsYXNzaWZpY2F0aW9uX2ZpdCA8LSBwYXJzbmlwOjpmaXQoY2xhc3NpZmljYXRpb25fd2YsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzaWZpY2F0aW9uX3RyYWluX2RmKQ0KDQpleHRyYWN0X2ZpdF9lbmdpbmUocmVncmVzc2lvbl9maXQpIHw+IA0KICB2aXA6OnZpcChudW1fZmVhdHVyZXMgPSAyMCkNCg0Kc3VibWlzc2lvbiA8LSBwcmVkaWN0KA0KICBjbGFzc2lmaWNhdGlvbl9maXQsDQogIA0KICBzdWJtaXRfZXNzYXlzX3JhdyB8Pg0KICAgIGJpbmRfY29scyhwcmVkaWN0KHJlZ3Jlc3Npb25fZml0LCBzdWJtaXRfZXNzYXlzX3JhdykpIHw+DQogICAgcmVuYW1lKHJlZ3Jlc3Npb25fcHJlZCA9IC5wcmVkKQ0KKSB8Pg0KICB0cmFuc211dGUoe3tvdXRjb21lfX0gOj0gLnByZWRfY2xhc3MpIHw+DQogIGJpbmRfY29scyhzdWJtaXNzaW9uKQ0KDQpzdWJtaXNzaW9uDQoNCmBgYA0KDQojIyMgUGhyYXNlb2xvZ3kNCg0KV2UgZml0IGZvciBgcGhyYXNlb2xvZ3lgIGZpcnN0IHVzaW5nIGFuIHhnYm9vc3QgcmVncmVzc2lvbiwgdXNpbmcgY2FzZSB3ZWlnaHRzIHRvIGFkanVzdCBmb3IgdGhlIGZyZXF1ZW5jeSBvZiBvY2N1cnJlbmNlIG9mIGVhY2ggdmFsdWUgb2YgYHBocmFzZW9sb2d5YC4gIA0KDQpgYGB7cn0NCiN8IGxhYmVsOiBmaXQgcGhyYXNlb2xvZ3kgcmVzYW1wbGVzIHJlZ3Jlc3Npb24NCg0Kb3V0Y29tZSA8LSBvdXRjb21lc1s0XQ0KDQpyZWdyZXNzaW9uX3RyYWluX2RmIDwtIHRyYWluX2Vzc2F5c19yYXcgIHw+IA0KICAgICAgICAgICAgICAgIHNlbGVjdCghIW91dGNvbWUsIGZ1bGxfdGV4dCkgfD4gDQogICAgICAgICAgICAgICAgY2FzZV93ZWlnaHRfYnVpbGRlcihvdXRjb21lKQ0KDQpyZWdyZXNzaW9uX3dmIDwtIHdvcmtmbG93KHJlY2lwZV9idWlsZGVyKG91dGNvbWUgPSBvdXRjb21lKSwgeGdiX3NwZWMpIHw+IA0KICAgICAgIGFkZF9jYXNlX3dlaWdodHMoY2FzZV93dHMpDQoNCmZvbGRzIDwtIHZmb2xkX2N2KHJlZ3Jlc3Npb25fdHJhaW5fZGYsIHN0cmF0YSA9IHt7b3V0Y29tZX19KQ0KDQpzZXQuc2VlZCg0MikgIA0KcnMgPC0gZml0X3Jlc2FtcGxlcygNCiAgcmVncmVzc2lvbl93ZiwNCiAgZm9sZHMsDQogIGNvbnRyb2wgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKSkNCg0KY29sbGVjdF9tZXRyaWNzKHJzKSB8PiBhcnJhbmdlKG1lYW4pDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBwbG90X3ByZWRzKC5kYXRhW1tvdXRjb21lXV0pICsNCiAgbGFicyh5ID0gIlByZWRpY3RlZCIsDQogICAgICAgdGl0bGUgPSBwYXN0ZTAob3V0Y29tZSwgIiBwcmVkaWN0aW9ucyBhZ2FpbnN0IGVzc2F5cyBpbiBoZWxkIG91dCBmb2xkcyIpLA0KICAgICAgIHN1YnRpdGxlID0gIlRoZSBoaWdoZXN0IGFuZCBsb3dlc3QgZXNzYXlzIGFyZSBub3QgcHJlZGljdGVkIHdlbGwiKQ0KDQoNCnRyYWluX2Vzc2F5c19yYXdbDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBtdXRhdGUocmVzaWR1YWwgPSAuZGF0YVtbb3V0Y29tZV1dIC0gLnByZWQpIHw+IA0KICBhcnJhbmdlKGRlc2MoYWJzKHJlc2lkdWFsKSkpIHw+IA0KICBzbGljZV9oZWFkKG4gPSA1KSB8PiANCiAgcHVsbCgucm93KQ0KDQosIF0gfD4gDQogIHNlbGVjdChmdWxsX3RleHQpDQoNCnJlZ3Jlc3Npb25fZml0IDwtIHBhcnNuaXA6OmZpdChyZWdyZXNzaW9uX3dmLCANCiAgICAgICAgICAgICAgICAgICAgIHJlZ3Jlc3Npb25fdHJhaW5fZGYpDQoNCmBgYA0KDQpJZGVudGlmeWluZyBleGFtcGxlcyB3aXRoIGVzcGVjaWFsbHkgcG9vciBwZXJmb3JtYW5jZSBjYW4gaGVscCB1cyBmb2xsb3cgdXAgYW5kIGludmVzdGlnYXRlIHdoeSB0aGVzZSBzcGVjaWZpYyBwcmVkaWN0aW9ucyBhcmUgcG9vci4gQ29uY2VwdHVhbGx5LCBpdHMgZWFzeSBmb3IgYSBiYXNlbGluZSBrbm93LW5vdGhpbmcgbW9kZWwgdG8gYXNzaWduIGFsbCBlc3NheXMgdG8gdGhlIG1lZGlhbiBzY29yZSBvZiAzLiBUaGUgcHJlZGljdGl2ZSBwb3dlciBpcyBpbiB0aGUgYWJpbGl0eSB0byBtb2RlbCB0aGUgZXNzYXlzIHRoYXQgYXJlIG5vdCAzIGludG8gYnVja2V0cyBoaWdoZXIgYW5kIGxvd2VyIHRoYW4gMy4NCg0KQmVjYXVzZSB0aGUgcmF0aW5ncyBhcmUgYSBmb3JtIG9mIG9yZGluYWwgdmFsdWUsIG9yIGV2ZW4gYSBsaWtlcnQgc2NhbGUsIHdlIHdpbGwgZW5zZW1ibGUgYSBzZWNvbmQgY2xhc3NpZmljYXRpb24gbW9kZWwgdGhhdCBpbmNsdWRlcyB0aGUgb3V0cHV0IG9mIHRoZSByZWdyZXNzaW9uLg0KDQpgYGB7cn0NCiN8IGxhYmVsOiBmaXQgcGhyYXNlb2xvZ3kgcmVzYW1wbGVzIGNsYXNzaWZpY2F0aW9uDQoNCmNsYXNzaWZpY2F0aW9uX3RyYWluX2RmIDwtIHRyYWluX2Vzc2F5c19yYXcgIHw+IA0KICAgICAgICAgICAgICAgIHNlbGVjdCh7e291dGNvbWV9fSwgZnVsbF90ZXh0KSB8PiANCiAgICAgICAgICAgICAgICBiaW5kX2NvbHMoDQogICAgICAgICAgICAgICAgICBwcmVkaWN0KA0KICAgICAgICAgICAgICAgICAgICByZWdyZXNzaW9uX2ZpdCwNCiAgICAgICAgICAgICAgICAgICAgcmVncmVzc2lvbl90cmFpbl9kZg0KICAgICAgICAgICAgICAgICAgKQ0KICAgICAgICAgICAgICAgICkgfD4gDQogICAgICAgICAgICAgICByZW5hbWUocmVncmVzc2lvbl9wcmVkID0gLnByZWQpIHw+IA0KICAgICAgICAgICAgICAgbXV0YXRlKHt7b3V0Y29tZX19IDo9IGZhY3RvciguZGF0YVtbb3V0Y29tZV1dKSkNCg0KY2xhc3NpZmljYXRpb25fd2YgPC0gd29ya2Zsb3cobXVsdGljbGFzc19yZWNpcGVfYnVpbGRlcihvdXRjb21lID0gb3V0Y29tZSksIHN2bV9zcGVjKSANCg0KZm9sZHMgPC0gdmZvbGRfY3YoY2xhc3NpZmljYXRpb25fdHJhaW5fZGYsIHN0cmF0YSA9ICEhb3V0Y29tZSkNCg0Kc2V0LnNlZWQoNDIpICANCnJzIDwtIGZpdF9yZXNhbXBsZXMoDQogIGNsYXNzaWZpY2F0aW9uX3dmLA0KICBmb2xkcywNCiAgbWV0cmljcyA9IG1ldHJpY19zZXQoa2FwLCBhY2N1cmFjeSksDQogIGNvbnRyb2wgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKSkNCg0KY29sbGVjdF9tZXRyaWNzKHJzKSB8PiBhcnJhbmdlKG1lYW4pDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBnZ3Bsb3QoYWVzKHggPSAuZGF0YVtbb3V0Y29tZV1dLCB5ID0gYWJzKGFzLm51bWVyaWMoLmRhdGFbW291dGNvbWVdXSkgLSBhcy5udW1lcmljKC5wcmVkX2NsYXNzKSkvMikpICsNCiAgZ2VvbV92aW9saW4oKSArDQogIHNjYWxlX3lfY29udGludW91cyhicmVha3MgPSBzZXEoLTUsNSwwLjUpKSArDQogIGxhYnMoeSA9ICJSZXNpZHVhbHMiLA0KICAgICAgIHRpdGxlID0gInt7b3V0Y29tZX19IFJlc2lkdWFsIGVycm9ycyBmb3IgZXNzYXlzIGluIGhlbGQgb3V0IGZvbGRzIiwNCiAgICAgICBzdWJ0aXRsZSA9ICJUaGUgaGlnaGVzdCBhbmQgbG93ZXN0IGVzc2F5cyBhcmUgc3RpbGwgbm90IHByZWRpY3RlZCB3ZWxsIikNCg0KdHJhaW5fZXNzYXlzX3Jhd1sNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhycykgfD4gDQogIG11dGF0ZShyZXNpZHVhbCA9IGFzLm51bWVyaWMoLmRhdGFbW291dGNvbWVdXSkgLSBhcy5udW1lcmljKC5wcmVkX2NsYXNzKSkgfD4gDQogIGFycmFuZ2UoZGVzYyhhYnMocmVzaWR1YWwpKSkgfD4gDQogIHNsaWNlX2hlYWQobiA9IDUpIHw+IA0KICBwdWxsKC5yb3cpDQoNCiwgXSB8PiANCiAgc2VsZWN0KGZ1bGxfdGV4dCwge3tvdXRjb21lfX0pDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBybXNlKHRydXRoID0gYXMubnVtZXJpYyguZGF0YVtbb3V0Y29tZV1dKS8yLCBlc3RpbWF0ZSA9IGFzLm51bWVyaWMoLnByZWRfY2xhc3MpLzIpDQoNCg0KYGBgDQoNClJlc3VsdHMgaGVyZSBhcmVuJ3QgZ3JlYXQsIGJ1dCB0aGV5IGFyZSBtb3JlIGFyZSBsZXNzIGNvbXBldGl0aXZlIHdpdGggdGhlIGxlYWRlcmJvYXJkIGZpZ3VyZXMuDQoNClRoZSBmaW5hbCBmaXR0aW5nIGVuc2VtYmxlcyBib3RoIHRoZSByZWdyZXNzaW9uIGFuZCBjbGFzc2lmaWNhdGlvbiBmaXRzLCBhbmQgbWFrZXMgYSBwcmVkaWN0aW9uIG9uIHRoZSBzdWJtaXNzaW9uIGVzc2F5cy4NCg0KYGBge3J9DQojfCBsYWJlbDogZml0IHBocmFzZW9sb2d5IGZpbmFsDQoNCmNsYXNzaWZpY2F0aW9uX2ZpdCA8LSBwYXJzbmlwOjpmaXQoY2xhc3NpZmljYXRpb25fd2YsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzaWZpY2F0aW9uX3RyYWluX2RmKQ0KDQpleHRyYWN0X2ZpdF9lbmdpbmUocmVncmVzc2lvbl9maXQpIHw+IA0KICB2aXA6OnZpcChudW1fZmVhdHVyZXMgPSAyMCkNCg0Kc3VibWlzc2lvbiA8LSBwcmVkaWN0KA0KICBjbGFzc2lmaWNhdGlvbl9maXQsDQogIA0KICBzdWJtaXRfZXNzYXlzX3JhdyB8Pg0KICAgIGJpbmRfY29scyhwcmVkaWN0KHJlZ3Jlc3Npb25fZml0LCBzdWJtaXRfZXNzYXlzX3JhdykpIHw+DQogICAgcmVuYW1lKHJlZ3Jlc3Npb25fcHJlZCA9IC5wcmVkKQ0KKSB8Pg0KICB0cmFuc211dGUoe3tvdXRjb21lfX0gOj0gLnByZWRfY2xhc3MpIHw+DQogIGJpbmRfY29scyhzdWJtaXNzaW9uKQ0KDQpzdWJtaXNzaW9uDQoNCmBgYA0KDQojIyMgR3JhbW1hcg0KDQpXZSBmaXQgZm9yIGBncmFtbWFyYCBmaXJzdCB1c2luZyBhbiB4Z2Jvb3N0IHJlZ3Jlc3Npb24sIHVzaW5nIGNhc2Ugd2VpZ2h0cyB0byBhZGp1c3QgZm9yIHRoZSBmcmVxdWVuY3kgb2Ygb2NjdXJyZW5jZSBvZiBlYWNoIHZhbHVlIG9mIGBncmFtbWFyYC4gIA0KDQpgYGB7cn0NCiN8IGxhYmVsOiBmaXQgZ3JhbW1hciByZXNhbXBsZXMgcmVncmVzc2lvbg0KDQpvdXRjb21lIDwtIG91dGNvbWVzWzVdDQoNCnJlZ3Jlc3Npb25fdHJhaW5fZGYgPC0gdHJhaW5fZXNzYXlzX3JhdyAgfD4gDQogICAgICAgICAgICAgICAgc2VsZWN0KCEhb3V0Y29tZSwgZnVsbF90ZXh0KSB8PiANCiAgICAgICAgICAgICAgICBjYXNlX3dlaWdodF9idWlsZGVyKG91dGNvbWUpDQoNCnJlZ3Jlc3Npb25fd2YgPC0gd29ya2Zsb3cocmVjaXBlX2J1aWxkZXIob3V0Y29tZSA9IG91dGNvbWUpLCB4Z2Jfc3BlYykgfD4gDQogICAgICAgYWRkX2Nhc2Vfd2VpZ2h0cyhjYXNlX3d0cykNCg0KZm9sZHMgPC0gdmZvbGRfY3YocmVncmVzc2lvbl90cmFpbl9kZiwgc3RyYXRhID0ge3tvdXRjb21lfX0pDQoNCnNldC5zZWVkKDQyKSAgDQpycyA8LSBmaXRfcmVzYW1wbGVzKA0KICByZWdyZXNzaW9uX3dmLA0KICBmb2xkcywNCiAgY29udHJvbCA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IFRSVUUpKQ0KDQpjb2xsZWN0X21ldHJpY3MocnMpIHw+IGFycmFuZ2UobWVhbikNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhycykgfD4gDQogIHBsb3RfcHJlZHMoLmRhdGFbW291dGNvbWVdXSkgKw0KICBsYWJzKHkgPSAiUHJlZGljdGVkIiwNCiAgICAgICB0aXRsZSA9IHBhc3RlMChvdXRjb21lLCAiIHByZWRpY3Rpb25zIGFnYWluc3QgZXNzYXlzIGluIGhlbGQgb3V0IGZvbGRzIiksDQogICAgICAgc3VidGl0bGUgPSAiVGhlIGhpZ2hlc3QgYW5kIGxvd2VzdCBlc3NheXMgYXJlIG5vdCBwcmVkaWN0ZWQgd2VsbCIpDQoNCg0KdHJhaW5fZXNzYXlzX3Jhd1sNCg0KY29sbGVjdF9wcmVkaWN0aW9ucyhycykgfD4gDQogIG11dGF0ZShyZXNpZHVhbCA9IC5kYXRhW1tvdXRjb21lXV0gLSAucHJlZCkgfD4gDQogIGFycmFuZ2UoZGVzYyhhYnMocmVzaWR1YWwpKSkgfD4gDQogIHNsaWNlX2hlYWQobiA9IDUpIHw+IA0KICBwdWxsKC5yb3cpDQoNCiwgXSB8PiANCiAgc2VsZWN0KGZ1bGxfdGV4dCkNCg0KcmVncmVzc2lvbl9maXQgPC0gcGFyc25pcDo6Zml0KHJlZ3Jlc3Npb25fd2YsIA0KICAgICAgICAgICAgICAgICAgICAgcmVncmVzc2lvbl90cmFpbl9kZikNCg0KYGBgDQoNCklkZW50aWZ5aW5nIGV4YW1wbGVzIHdpdGggZXNwZWNpYWxseSBwb29yIHBlcmZvcm1hbmNlIGNhbiBoZWxwIHVzIGZvbGxvdyB1cCBhbmQgaW52ZXN0aWdhdGUgd2h5IHRoZXNlIHNwZWNpZmljIHByZWRpY3Rpb25zIGFyZSBwb29yLiBDb25jZXB0dWFsbHksIGl0cyBlYXN5IGZvciBhIGJhc2VsaW5lIGtub3ctbm90aGluZyBtb2RlbCB0byBhc3NpZ24gYWxsIGVzc2F5cyB0byB0aGUgbWVkaWFuIHNjb3JlIG9mIDMuIFRoZSBwcmVkaWN0aXZlIHBvd2VyIGlzIGluIHRoZSBhYmlsaXR5IHRvIG1vZGVsIHRoZSBlc3NheXMgdGhhdCBhcmUgbm90IDMgaW50byBidWNrZXRzIGhpZ2hlciBhbmQgbG93ZXIgdGhhbiAzLg0KDQpCZWNhdXNlIHRoZSByYXRpbmdzIGFyZSBhIGZvcm0gb2Ygb3JkaW5hbCB2YWx1ZSwgb3IgZXZlbiBhIGxpa2VydCBzY2FsZSwgd2Ugd2lsbCBlbnNlbWJsZSBhIHNlY29uZCBjbGFzc2lmaWNhdGlvbiBtb2RlbCB0aGF0IGluY2x1ZGVzIHRoZSBvdXRwdXQgb2YgdGhlIHJlZ3Jlc3Npb24uDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IGZpdCBncmFtbWFyIHJlc2FtcGxlcyBjbGFzc2lmaWNhdGlvbg0KDQpjbGFzc2lmaWNhdGlvbl90cmFpbl9kZiA8LSB0cmFpbl9lc3NheXNfcmF3ICB8PiANCiAgICAgICAgICAgICAgICBzZWxlY3Qoe3tvdXRjb21lfX0sIGZ1bGxfdGV4dCkgfD4gDQogICAgICAgICAgICAgICAgYmluZF9jb2xzKA0KICAgICAgICAgICAgICAgICAgcHJlZGljdCgNCiAgICAgICAgICAgICAgICAgICAgcmVncmVzc2lvbl9maXQsDQogICAgICAgICAgICAgICAgICAgIHJlZ3Jlc3Npb25fdHJhaW5fZGYNCiAgICAgICAgICAgICAgICAgICkNCiAgICAgICAgICAgICAgICApIHw+IA0KICAgICAgICAgICAgICAgcmVuYW1lKHJlZ3Jlc3Npb25fcHJlZCA9IC5wcmVkKSB8PiANCiAgICAgICAgICAgICAgIG11dGF0ZSh7e291dGNvbWV9fSA6PSBmYWN0b3IoLmRhdGFbW291dGNvbWVdXSkpDQoNCmNsYXNzaWZpY2F0aW9uX3dmIDwtIHdvcmtmbG93KG11bHRpY2xhc3NfcmVjaXBlX2J1aWxkZXIob3V0Y29tZSA9IG91dGNvbWUpLCBzdm1fc3BlYykgDQoNCmZvbGRzIDwtIHZmb2xkX2N2KGNsYXNzaWZpY2F0aW9uX3RyYWluX2RmLCBzdHJhdGEgPSAhIW91dGNvbWUpDQoNCnNldC5zZWVkKDQyKSAgDQpycyA8LSBmaXRfcmVzYW1wbGVzKA0KICBjbGFzc2lmaWNhdGlvbl93ZiwNCiAgZm9sZHMsDQogIG1ldHJpY3MgPSBtZXRyaWNfc2V0KGthcCwgYWNjdXJhY3kpLA0KICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkpDQoNCmNvbGxlY3RfbWV0cmljcyhycykgfD4gYXJyYW5nZShtZWFuKQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgZ2dwbG90KGFlcyh4ID0gLmRhdGFbW291dGNvbWVdXSwgeSA9IGFicyhhcy5udW1lcmljKC5kYXRhW1tvdXRjb21lXV0pIC0gYXMubnVtZXJpYygucHJlZF9jbGFzcykpLzIpKSArDQogIGdlb21fdmlvbGluKCkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKC01LDUsMC41KSkgKw0KICBsYWJzKHkgPSAiUmVzaWR1YWxzIiwNCiAgICAgICB0aXRsZSA9ICJ7e291dGNvbWV9fSBSZXNpZHVhbCBlcnJvcnMgZm9yIGVzc2F5cyBpbiBoZWxkIG91dCBmb2xkcyIsDQogICAgICAgc3VidGl0bGUgPSAiVGhlIGhpZ2hlc3QgYW5kIGxvd2VzdCBlc3NheXMgYXJlIHN0aWxsIG5vdCBwcmVkaWN0ZWQgd2VsbCIpDQoNCnRyYWluX2Vzc2F5c19yYXdbDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBtdXRhdGUocmVzaWR1YWwgPSBhcy5udW1lcmljKC5kYXRhW1tvdXRjb21lXV0pIC0gYXMubnVtZXJpYygucHJlZF9jbGFzcykpIHw+IA0KICBhcnJhbmdlKGRlc2MoYWJzKHJlc2lkdWFsKSkpIHw+IA0KICBzbGljZV9oZWFkKG4gPSA1KSB8PiANCiAgcHVsbCgucm93KQ0KDQosIF0gfD4gDQogIHNlbGVjdChmdWxsX3RleHQsIHt7b3V0Y29tZX19KQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgcm1zZSh0cnV0aCA9IGFzLm51bWVyaWMoLmRhdGFbW291dGNvbWVdXSkvMiwgZXN0aW1hdGUgPSBhcy5udW1lcmljKC5wcmVkX2NsYXNzKS8yKQ0KDQpgYGANCg0KUmVzdWx0cyBoZXJlIGFyZW4ndCBncmVhdCwgYnV0IHRoZXkgYXJlIG1vcmUgYXJlIGxlc3MgY29tcGV0aXRpdmUgd2l0aCB0aGUgbGVhZGVyYm9hcmQgZmlndXJlcy4NCg0KVGhlIGZpbmFsIGZpdHRpbmcgZW5zZW1ibGVzIGJvdGggdGhlIHJlZ3Jlc3Npb24gYW5kIGNsYXNzaWZpY2F0aW9uIGZpdHMsIGFuZCBtYWtlcyBhIHByZWRpY3Rpb24gb24gdGhlIHN1Ym1pc3Npb24gZXNzYXlzLg0KDQpgYGB7cn0NCiN8IGxhYmVsOiBmaXQgZ3JhbW1hciBmaW5hbA0KDQpjbGFzc2lmaWNhdGlvbl9maXQgPC0gcGFyc25pcDo6Zml0KGNsYXNzaWZpY2F0aW9uX3dmLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc2lmaWNhdGlvbl90cmFpbl9kZikNCg0KZXh0cmFjdF9maXRfZW5naW5lKHJlZ3Jlc3Npb25fZml0KSB8PiANCiAgdmlwOjp2aXAobnVtX2ZlYXR1cmVzID0gMjApDQoNCnN1Ym1pc3Npb24gPC0gcHJlZGljdCgNCiAgY2xhc3NpZmljYXRpb25fZml0LA0KICANCiAgc3VibWl0X2Vzc2F5c19yYXcgfD4NCiAgICBiaW5kX2NvbHMocHJlZGljdChyZWdyZXNzaW9uX2ZpdCwgc3VibWl0X2Vzc2F5c19yYXcpKSB8Pg0KICAgIHJlbmFtZShyZWdyZXNzaW9uX3ByZWQgPSAucHJlZCkNCikgfD4NCiAgdHJhbnNtdXRlKHt7b3V0Y29tZX19IDo9IC5wcmVkX2NsYXNzKSB8Pg0KICBiaW5kX2NvbHMoc3VibWlzc2lvbikNCg0Kc3VibWlzc2lvbg0KDQpgYGANCg0KIyMjIENvbnZlbnRpb25zDQoNCldlIGZpdCBmb3IgYGNvbnZlbnRpb25zYCBmaXJzdCB1c2luZyBhbiB4Z2Jvb3N0IHJlZ3Jlc3Npb24sIHVzaW5nIGNhc2Ugd2VpZ2h0cyB0byBhZGp1c3QgZm9yIHRoZSBmcmVxdWVuY3kgb2Ygb2NjdXJyZW5jZSBvZiBlYWNoIHZhbHVlIG9mIGBjb252ZW50aW9uc2AuICANCg0KYGBge3J9DQojfCBsYWJlbDogZml0IGNvbnZlbnRpb25zIHJlc2FtcGxlcyByZWdyZXNzaW9uDQoNCm91dGNvbWUgPC0gb3V0Y29tZXNbNl0NCg0KcmVncmVzc2lvbl90cmFpbl9kZiA8LSB0cmFpbl9lc3NheXNfcmF3ICB8PiANCiAgICAgICAgICAgICAgICBzZWxlY3QoISFvdXRjb21lLCBmdWxsX3RleHQpIHw+IA0KICAgICAgICAgICAgICAgIGNhc2Vfd2VpZ2h0X2J1aWxkZXIob3V0Y29tZSkNCg0KcmVncmVzc2lvbl93ZiA8LSB3b3JrZmxvdyhyZWNpcGVfYnVpbGRlcihvdXRjb21lID0gb3V0Y29tZSksIHhnYl9zcGVjKSB8PiANCiAgICAgICBhZGRfY2FzZV93ZWlnaHRzKGNhc2Vfd3RzKQ0KDQpmb2xkcyA8LSB2Zm9sZF9jdihyZWdyZXNzaW9uX3RyYWluX2RmLCBzdHJhdGEgPSB7e291dGNvbWV9fSkNCg0Kc2V0LnNlZWQoNDIpICANCnJzIDwtIGZpdF9yZXNhbXBsZXMoDQogIHJlZ3Jlc3Npb25fd2YsDQogIGZvbGRzLA0KICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkpDQoNCmNvbGxlY3RfbWV0cmljcyhycykgfD4gYXJyYW5nZShtZWFuKQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgcGxvdF9wcmVkcyguZGF0YVtbb3V0Y29tZV1dKSArDQogIGxhYnMoeSA9ICJQcmVkaWN0ZWQiLA0KICAgICAgIHRpdGxlID0gcGFzdGUwKG91dGNvbWUsICIgcHJlZGljdGlvbnMgYWdhaW5zdCBlc3NheXMgaW4gaGVsZCBvdXQgZm9sZHMiKSwNCiAgICAgICBzdWJ0aXRsZSA9ICJUaGUgaGlnaGVzdCBhbmQgbG93ZXN0IGVzc2F5cyBhcmUgbm90IHByZWRpY3RlZCB3ZWxsIikNCg0KDQp0cmFpbl9lc3NheXNfcmF3Ww0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgbXV0YXRlKHJlc2lkdWFsID0gLmRhdGFbW291dGNvbWVdXSAtIC5wcmVkKSB8PiANCiAgYXJyYW5nZShkZXNjKGFicyhyZXNpZHVhbCkpKSB8PiANCiAgc2xpY2VfaGVhZChuID0gNSkgfD4gDQogIHB1bGwoLnJvdykNCg0KLCBdIHw+IA0KICBzZWxlY3QoZnVsbF90ZXh0KQ0KDQpyZWdyZXNzaW9uX2ZpdCA8LSBwYXJzbmlwOjpmaXQocmVncmVzc2lvbl93ZiwgDQogICAgICAgICAgICAgICAgICAgICByZWdyZXNzaW9uX3RyYWluX2RmKQ0KDQpgYGANCg0KSWRlbnRpZnlpbmcgZXhhbXBsZXMgd2l0aCBlc3BlY2lhbGx5IHBvb3IgcGVyZm9ybWFuY2UgY2FuIGhlbHAgdXMgZm9sbG93IHVwIGFuZCBpbnZlc3RpZ2F0ZSB3aHkgdGhlc2Ugc3BlY2lmaWMgcHJlZGljdGlvbnMgYXJlIHBvb3IuIENvbmNlcHR1YWxseSwgaXRzIGVhc3kgZm9yIGEgYmFzZWxpbmUga25vdy1ub3RoaW5nIG1vZGVsIHRvIGFzc2lnbiBhbGwgZXNzYXlzIHRvIHRoZSBtZWRpYW4gc2NvcmUgb2YgMy4gVGhlIHByZWRpY3RpdmUgcG93ZXIgaXMgaW4gdGhlIGFiaWxpdHkgdG8gbW9kZWwgdGhlIGVzc2F5cyB0aGF0IGFyZSBub3QgMyBpbnRvIGJ1Y2tldHMgaGlnaGVyIGFuZCBsb3dlciB0aGFuIDMuDQoNCkJlY2F1c2UgdGhlIHJhdGluZ3MgYXJlIGEgZm9ybSBvZiBvcmRpbmFsIHZhbHVlLCBvciBldmVuIGEgbGlrZXJ0IHNjYWxlLCB3ZSB3aWxsIGVuc2VtYmxlIGEgc2Vjb25kIGNsYXNzaWZpY2F0aW9uIG1vZGVsIHRoYXQgaW5jbHVkZXMgdGhlIG91dHB1dCBvZiB0aGUgcmVncmVzc2lvbi4NCg0KYGBge3J9DQojfCBsYWJlbDogZml0IGNvbnZlbnRpb25zIHJlc2FtcGxlcyBjbGFzc2lmaWNhdGlvbg0KDQpjbGFzc2lmaWNhdGlvbl90cmFpbl9kZiA8LSB0cmFpbl9lc3NheXNfcmF3ICB8PiANCiAgICAgICAgICAgICAgICBzZWxlY3Qoe3tvdXRjb21lfX0sIGZ1bGxfdGV4dCkgfD4gDQogICAgICAgICAgICAgICAgYmluZF9jb2xzKA0KICAgICAgICAgICAgICAgICAgcHJlZGljdCgNCiAgICAgICAgICAgICAgICAgICAgcmVncmVzc2lvbl9maXQsDQogICAgICAgICAgICAgICAgICAgIHJlZ3Jlc3Npb25fdHJhaW5fZGYNCiAgICAgICAgICAgICAgICAgICkNCiAgICAgICAgICAgICAgICApIHw+IA0KICAgICAgICAgICAgICAgcmVuYW1lKHJlZ3Jlc3Npb25fcHJlZCA9IC5wcmVkKSB8PiANCiAgICAgICAgICAgICAgIG11dGF0ZSh7e291dGNvbWV9fSA6PSBmYWN0b3IoLmRhdGFbW291dGNvbWVdXSkpDQoNCmNsYXNzaWZpY2F0aW9uX3dmIDwtIHdvcmtmbG93KG11bHRpY2xhc3NfcmVjaXBlX2J1aWxkZXIob3V0Y29tZSA9IG91dGNvbWUpLCBzdm1fc3BlYykgDQoNCmZvbGRzIDwtIHZmb2xkX2N2KGNsYXNzaWZpY2F0aW9uX3RyYWluX2RmLCBzdHJhdGEgPSAhIW91dGNvbWUpDQoNCnNldC5zZWVkKDQyKSAgDQpycyA8LSBmaXRfcmVzYW1wbGVzKA0KICBjbGFzc2lmaWNhdGlvbl93ZiwNCiAgZm9sZHMsDQogIG1ldHJpY3MgPSBtZXRyaWNfc2V0KGthcCwgYWNjdXJhY3kpLA0KICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkpDQoNCmNvbGxlY3RfbWV0cmljcyhycykgfD4gYXJyYW5nZShtZWFuKQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgZ2dwbG90KGFlcyh4ID0gLmRhdGFbW291dGNvbWVdXSwgeSA9IGFicyhhcy5udW1lcmljKC5kYXRhW1tvdXRjb21lXV0pIC0gYXMubnVtZXJpYygucHJlZF9jbGFzcykpLzIpKSArDQogIGdlb21fdmlvbGluKCkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKC01LDUsMC41KSkgKw0KICBsYWJzKHkgPSAiUmVzaWR1YWxzIiwNCiAgICAgICB0aXRsZSA9ICJ7e291dGNvbWV9fSBSZXNpZHVhbCBlcnJvcnMgZm9yIGVzc2F5cyBpbiBoZWxkIG91dCBmb2xkcyIsDQogICAgICAgc3VidGl0bGUgPSAiVGhlIGhpZ2hlc3QgYW5kIGxvd2VzdCBlc3NheXMgYXJlIHN0aWxsIG5vdCBwcmVkaWN0ZWQgd2VsbCIpDQoNCnRyYWluX2Vzc2F5c19yYXdbDQoNCmNvbGxlY3RfcHJlZGljdGlvbnMocnMpIHw+IA0KICBtdXRhdGUocmVzaWR1YWwgPSBhcy5udW1lcmljKC5kYXRhW1tvdXRjb21lXV0pIC0gYXMubnVtZXJpYygucHJlZF9jbGFzcykpIHw+IA0KICBhcnJhbmdlKGRlc2MoYWJzKHJlc2lkdWFsKSkpIHw+IA0KICBzbGljZV9oZWFkKG4gPSA1KSB8PiANCiAgcHVsbCgucm93KQ0KDQosIF0gfD4gDQogIHNlbGVjdChmdWxsX3RleHQsIHt7b3V0Y29tZX19KQ0KDQpjb2xsZWN0X3ByZWRpY3Rpb25zKHJzKSB8PiANCiAgcm1zZSh0cnV0aCA9IGFzLm51bWVyaWMoLmRhdGFbW291dGNvbWVdXSkvMiwgZXN0aW1hdGUgPSBhcy5udW1lcmljKC5wcmVkX2NsYXNzKS8yKQ0KDQoNCmBgYA0KDQpSZXN1bHRzIGhlcmUgYXJlbid0IGdyZWF0LCBidXQgdGhleSBhcmUgbW9yZSBhcmUgbGVzcyBjb21wZXRpdGl2ZSB3aXRoIHRoZSBsZWFkZXJib2FyZCBmaWd1cmVzLg0KDQpUaGUgZmluYWwgZml0dGluZyBlbnNlbWJsZXMgYm90aCB0aGUgcmVncmVzc2lvbiBhbmQgY2xhc3NpZmljYXRpb24gZml0cywgYW5kIG1ha2VzIGEgcHJlZGljdGlvbiBvbiB0aGUgc3VibWlzc2lvbiBlc3NheXMuDQoNCmBgYHtyfQ0KI3wgbGFiZWw6IGZpdCBjb252ZW50aW9ucyBmaW5hbA0KDQpjbGFzc2lmaWNhdGlvbl9maXQgPC0gcGFyc25pcDo6Zml0KGNsYXNzaWZpY2F0aW9uX3dmLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzc2lmaWNhdGlvbl90cmFpbl9kZikNCg0KZXh0cmFjdF9maXRfZW5naW5lKHJlZ3Jlc3Npb25fZml0KSB8PiANCiAgdmlwOjp2aXAobnVtX2ZlYXR1cmVzID0gMjApDQoNCnN1Ym1pc3Npb24gPC0gcHJlZGljdCgNCiAgY2xhc3NpZmljYXRpb25fZml0LA0KICANCiAgc3VibWl0X2Vzc2F5c19yYXcgfD4NCiAgICBiaW5kX2NvbHMocHJlZGljdChyZWdyZXNzaW9uX2ZpdCwgc3VibWl0X2Vzc2F5c19yYXcpKSB8Pg0KICAgIHJlbmFtZShyZWdyZXNzaW9uX3ByZWQgPSAucHJlZCkNCikgfD4NCiAgdHJhbnNtdXRlKHt7b3V0Y29tZX19IDo9IC5wcmVkX2NsYXNzKSB8Pg0KICBiaW5kX2NvbHMoc3VibWlzc2lvbikNCg0Kc3VibWlzc2lvbg0KDQpgYGANCg0KIyMgey19DQoNCiMgVGhlIFN1Ym1pc3Npb24NCg0KS2FnZ2xlJ3Mgc3lzdGVtIHJ1bnMgdGhlIHdvcmtib29rIHR3aWNlLiBUaGUgZmlyc3QgdGltZSBpcyBvbiB0aGUgdGlueSB0aHJlZSBsaW5lIHB1YmxpYyB0ZXN0IGRhdGFzZXQgaGVyZS4gVGhlIHNlY29uZCB0aW1lIGlzIG9uIGEgbXVjaCBtdWNoIGxhcmdlciBoaWRkZW4gdGVzdCBkYXRhc2V0LiAgQXMgYSBjaGVjayB0byBzaW11bGF0ZSBob3cgdGhlIGhpZGRlbiBkYXRzZXQgbWlnaHQgZml0LCB3ZSBjb3VsZCByZS1maXQgb24gdGhlIHRyYWluIGRhdGFzZXQgdGV4dCBhY3Jvc3MgYWxsIG9mIHRoZSBmaXRzLg0KDQpgYGB7cn0NCiN8IGxhYmVsOiB3cml0ZSBzdWJtaXNzaW9uIG91dCBhcyBhIGNzdg0KDQpzdWJtaXNzaW9uDQoNCiMgd3JpdGVfY3N2KHN1Ym1pc3Npb24sIGhlcmU6OmhlcmUoImRhdGEiLCAic3VibWlzc2lvbi5jc3YiKSkNCmBgYA0KDQojIE91dGNvbWUNCg0KTm90IG9ubHkgd2FzIHRoaXMgZXhlcmNpc2UgYSBnb29kIHN0dWR5IG9mIExpa2VydCBldmFsdWF0aW9uIGRhdGEsIGJ1dCBhbHNvIG9mIE5MUCB0ZWNobmlxdWVzIGFuZCBvZiBzdGF0aXN0aWNhbCByZXNhbXBsaW5nIHRvIGFzc3VyZSB0aGF0IHRoZSBtb2RlbCBwZXJmb3JtcyBvbiB1bnNlZW4gZGF0YS4gIFRoZSByZXN1bHRpbmcgbW9kZWxzIGhlcmUgbGFjayB0aGUgcHJlZGljdGl2ZSBwb3dlciBuZWVkZWQgZm9yIHByb2R1Y3Rpb24gdXNlLiANCg0KDQoNCg0KDQoNCg0K