In this episode of SLICED, contestants are challenged to predict the price of Airbnb listings in NYC.

The evaluation metric for this competition is Root Mean Squared Logarithmic Error.

Split Data

set.seed(2021)
# Load required libraries
library(tidyverse)
library(tidymodels)
library(here)
library(textrecipes)
library(stacks)
library(broom)

# Import data
play_data <- read_csv("train.csv", show_col_types = FALSE) %>% 
  # Account for 0 by adding 1 
  mutate(price = log(price + 1))

holdout_data <- read_csv("test.csv", show_col_types = FALSE)

# Function for predicting on the hold out data
predict_holdout <- function(wf){
  wf %>% augment(holdout_data) %>% 
    mutate(.pred = exp(.pred) - 1) %>% 
    select(id, price = .pred)
}


# Function to make augment() work with stacks
augment.model_stack <- function(stack_object, data){
  data %>% 
    bind_cols(predict(stack_object, .))
}


# Split data
data_split <- initial_split(play_data, prop = 0.75)

# Extract data from split data
train <- training(data_split)
test <- testing(data_split)

EDA

# Brief summary of the variables
train %>% 
  glimpse()
## Rows: 25,669
## Columns: 16
## $ id                             <dbl> 26155827, 2298373, 2954785, 18550867, 6~
## $ name                           <chr> "Greenpoint room", "Luxury private apar~
## $ host_id                        <dbl> 5213686, 10149317, 15081041, 128785279,~
## $ host_name                      <chr> "Charlotte", "Lana", "Jaclyn", "Dusean"~
## $ neighbourhood_group            <chr> "Brooklyn", "Queens", "Manhattan", "Bro~
## $ neighbourhood                  <chr> "Greenpoint", "Rego Park", "Murray Hill~
## $ latitude                       <dbl> 40.73442, 40.72988, 40.74551, 40.85878,~
## $ longitude                      <dbl> -73.95854, -73.85773, -73.97757, -73.83~
## $ room_type                      <chr> "Private room", "Entire home/apt", "Ent~
## $ price                          <dbl> 4.262680, 4.488636, 5.135798, 5.298317,~
## $ minimum_nights                 <dbl> 5, 3, 5, 2, 2, 3, 3, 20, 30, 2, 1, 29, ~
## $ number_of_reviews              <dbl> 2, 33, 0, 81, 203, 2, 15, 0, 1, 6, 165,~
## $ last_review                    <date> 2018-08-09, 2019-04-20, NA, 2019-06-26~
## $ reviews_per_month              <dbl> 0.16, 0.77, NA, 3.17, 1.96, 1.13, 1.18,~
## $ calculated_host_listings_count <dbl> 1, 5, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, ~
## $ availability_365               <dbl> 0, 262, 0, 286, 86, 4, 365, 40, 342, 5,~

Distribution of what we are trying to predict

theme_set(theme_light())
train %>% 
  # Has 0s in it so we add 1
  ggplot(mapping = aes(x = price)) +
  geom_histogram(fill = "midnightblue", alpha = 0.7) 

# Find median and average prices
summary_price <- function(tbl){
  tbl %>% 
    summarize(avg_price = exp(mean(price))-1,
              n = n()) %>% arrange(desc(n))
              
}

train %>% 
  summary_price()
# How average price varies per neighbourhood_group
train %>% 
  mutate(neighbourhood_group = fct_reorder(neighbourhood_group, price)) %>% 
  ggplot(mapping = aes(x = exp(price), y = neighbourhood_group )) +
  geom_boxplot() +
  scale_x_log10()

# How average price varies per neighbourhood
train %>% 
  mutate(neighbourhood = fct_lump(neighbourhood, 40),
    neighbourhood = fct_reorder(neighbourhood, price)) %>% 
  ggplot(mapping = aes(x = exp(price), y = neighbourhood )) +
  geom_boxplot() +
  scale_x_log10()

# How price varies with room type
train %>% 
  mutate(room_type = fct_reorder(room_type, price)) %>% 
  ggplot(mapping = aes(x = exp(price), y = room_type )) +
  geom_boxplot() +
  scale_x_log10()

# How price varies with reviews per month
train %>% 
  ggplot(mapping = aes(x = number_of_reviews, y = price )) +
  geom_point() +
  scale_x_log10() +
  geom_smooth(method = "lm")

# How price varies with reviews per month
train %>% 
  ggplot(mapping = aes(x = reviews_per_month, y = price )) +
  geom_point() +
  scale_x_log10() +
  geom_smooth(method = "lm")

# How price varies with last review
train %>% 
  ggplot(mapping = aes(x = last_review, y = price )) +
  geom_point() +
  geom_smooth(method = "lm")

# How price varies with number of minimum nights
train %>% 
  slice_sample(n = 1000) %>% 
  ggplot(mapping = aes(x = minimum_nights, y = price )) +
  geom_point() +
  scale_x_log10() +
  geom_smooth(method = "lm")

Text Analysis

library(tidytext)

# Word tokenization to see relationship between word and price
train %>% 
  unnest_tokens(input = name, output = word, token = "words") %>% 
  group_by(word) %>% 
  summary_price() %>% 
  head(50) %>% 
  mutate(word = fct_reorder(word, avg_price)) %>% 
  ggplot(aes(x = avg_price, y = word, size = n)) +
  geom_point()

Simple map

library(ggmap)
library(ggthemes)

# Aggregate nearby areas
agg_lat_long <- train %>% 
  group_by(latitude = round(latitude, 2),
           longitude = round(longitude, 2)) %>% 
  summarize(price = mean(price), n = n()) %>% 
  filter(n >= 5)

# Find range of coverage
range(train %>% pull(latitude))
## [1] 40.50641 40.91306
range(train %>% pull(longitude))
## [1] -74.24285 -73.71690
bbox <- c(left = -74.24285,  bottom = 40.50641, right = -73.71690, top = 40.91306 )
nyc_map <- get_stamenmap(bbox, zoom = 11)

# Which areas have high prices
ggmap(nyc_map) +
  geom_point(data = agg_lat_long, aes(longitude, latitude, color = exp(price) - 1, size = n)) +
  scale_color_gradient2(low = "blue", high = "red", midpoint = 2, trans = "log10") +
  theme_map() +
  labs(color = "Price")

  • Presence of certain words really does matter in terms of pricing

  • Areas in certain long and lat have distinctive high prices

  • Seems there is a disparity of prices depending on the neighbourhood group and neighbourhood

  • The room type clearly partitions the house prices

  • The other features don’t seem to influence the price so much ?

Train xgboost model

Creates a series of decision trees forming an ensemble. Each tree depends on the results of previous trees in an attempt to reduce the error. All trees in the ensemble are combined to produce a final prediction.

# Control aspects of a grid search
eval_met <- metric_set(rmse)
grid_control <- control_grid(
  # Save predictions for each model evaluated
  save_pred = TRUE,
  # Save workflow (model+recipe)
  save_workflow = TRUE,
  # Extract a fitted model
  extract = extract_model)

# Resampling techniques for creating simulated data sets
set.seed(2021)
train_5fold_cv <- train %>% 
  vfold_cv(v = 5, repeats = 1)
train_5fold_cv
  • Does xgboost require us to normalize or transform variables to be more symmetric? Not really. More research on this

  • While tree-based boosting methods generally do not require the creation of dummy variables, models using the xgboost engine do.

# Function that applies preprocessing steps to data (sanity check)
prep_bake <- function(recipe) juice(prep(recipe))

# Data preprocessing with recipes - MOSTLY NUMERIC
xg_rec <- recipe(price ~ minimum_nights + room_type + number_of_reviews + latitude + longitude + neighbourhood_group + reviews_per_month + calculated_host_listings_count + availability_365 + last_review, data = train) %>% 
  # Make variables symmetrical
  #step_log(all_nominal_predictors(), offset = 1) %>% 
  # if not ever reviewed, 0
  # Narrow down to Manhattan as suggested by the VIP plot below
  step_mutate(manhattan_grp = neighbourhood_group == "Manhattan") %>% 
  step_rm(neighbourhood_group) %>% 
  step_mutate(last_review = coalesce(as.integer(Sys.Date() - last_review), 0)) %>% 
  step_dummy(all_nominal_predictors())

# Sanity check
# xg_rec %>% 
#   prep_bake() %>% 
#   slice_head(n = 5)

# Make a model specification
xg_mod <- boost_tree(
  # Randomly selected predictors at each split
  mtry = tune(),
  # Number of trees
  trees = tune(),
  learn_rate = 0.01
) %>% 
  set_engine("xgboost") %>% 
  set_mode("regression")

# Combine model specifcation and recipe into a workflow
xg_wf <- workflow() %>% 
  add_recipe(xg_rec) %>% 
  add_model(xg_mod)

# Tune model hyperparameters
doParallel::registerDoParallel()
xg_tune <- xg_wf %>% 
  tune_grid(
    resamples = train_5fold_cv,
    metrics = eval_met,
    control = grid_control,
    grid = crossing(mtry = c(4, 5, 6,  7), trees = seq(250, 1000, 25))
  )

xg_tune %>% autoplot()

# See performance metrics
xg_tune %>% 
  collect_metrics() %>% 
  arrange((mean))
  • RMSE of 0.535 with 600 trees and mtry = 2: price ~ minimum_nights + room_type + number_of_reviews

  • RMSE of 0.468 with 1000 trees and mtry = 6: price ~ minimum_nights + room_type + number_of_reviews + latitude + longitude + neighbourhood_group - do we need 1000 trees? Overfitting??

  • RMSE of 0.442 with 800 trees and mtry = 6: price ~ minimum_nights + room_type + number_of_reviews + latitude + longitude + neighbourhood_group + reviews_per_month + calculated_host_listings_count + availability_365

  • RMSE of 0.4418 with 800 trees and mtry = 7: price ~ minimum_nights + room_type + number_of_reviews + latitude + longitude + neighbourhood_group + reviews_per_month + calculated_host_listings_count + availability_365 + last_review — Finalize this

  • RMSE of 0.4413 with 1000 trees and mtry = 7: price ~ minimum_nights + room_type + number_of_reviews + latitude + longitude + neighbourhood_group + reviews_per_month + calculated_host_listings_count + availability_365 + last_review — with manhattan as a feature

Finalize version without many categorical values

# Finalize workflow and fit model
xg_fit <- xg_wf %>% 
  finalize_workflow(select_best(xg_tune)) %>% 
  fit(train)

# Evaluate model performance on test set
xg_fit %>% 
  augment(test) %>% 
  rmse(truth = price, estimate = .pred)
# Variable importance plot
important_var <- xgboost::xgb.importance(model = xg_fit$fit$fit$fit)

important_var %>% 
  mutate(Feature = fct_reorder(Feature, Gain)) %>% 
  ggplot(mapping = aes(x = Gain, y = Feature)) +
  geom_col()

# Make predictions on holdout data
xg_fit %>% 
  predict_holdout() %>% 
  write_csv("xg_numeric")

The Manhattan neighbourhood group stands out from the rest of its peers.

Linear version on the categorical

  • Additionally, step_other() can be used to analyze the frequencies of the factor levels in the training set and convert infrequently occurring values to a catch-all level of “other”, with a specific threshold that can be specified.

    Lumps infrequently occurring observations into a new level called other.

  • Normalize to compare them on the same scale later

# Recipe for preprocessing data
lin_rec <- recipe(price ~ name + room_type +
                    latitude + longitude +
                    neighbourhood_group + neighbourhood +
                    host_id, data = train) %>% 
  # Tokenize names
  step_tokenize(name) %>% 
  # Narrow down to top 100 words?
  step_tokenfilter(name, max_tokens = tune()) %>% 
  # Find if a token is found in the column - dumminise 
  step_tf(name) %>% 
  step_mutate(host_id = factor(host_id)) %>% 
  step_other(host_id, neighbourhood, threshold = tune()) %>% 
  step_dummy(all_nominal_predictors())

# Linear model specification
lin_mod <- linear_reg(penalty = tune()) %>% 
  set_engine("glmnet") %>% 
  set_mode("regression")

# Bundle model spec + recipe into workflow
lin_wf <- workflow() %>% 
  add_recipe(lin_rec) %>% 
  add_model(lin_mod)

# Model tuning
# Tune model hyperparameters
doParallel::registerDoParallel()
lin_tune <- lin_wf %>% 
  tune_grid(
    resamples = train_5fold_cv,
    metrics = eval_met,
    control = grid_control,
    grid = crossing(penalty = 10 ^ seq(-7, -1, .1),
                    threshold = c(0.0008,0.001),
                    max_tokens = c(100, 500)))

autoplot(lin_tune)

# Collect metrics
lin_tune %>% 
  collect_metrics() %>% 
  arrange(mean)
  • About 0.48 on room_type + latitude + longitude + neighbourhood_group + neighbourhood + host_id . Seems lower threshold(step_other) does better

  • Bringing in top 100 words improved performance. Trying to tune the max_tokens. Settle for a threshold of 0.001 (0.1%?) Resulted in 0.45

# Finalize worklow and train model
lin_fit <- lin_wf %>% 
  finalize_workflow(select_best(lin_tune)) %>% 
  fit(train)

# Make predictions on test set
lin_fit %>% 
  augment(test) %>% 
  rmse(truth = price, estimate = .pred)

Wow! Almost as good as xg_boost – 0.437

Now, let’s interpret the model and see which variables contributed to prediction

# Interpret model
library(broom)

lin_fit$fit$fit$fit %>% 
  tidy() %>% 
  filter(lambda >= select_best(lin_tune)$penalty) %>% 
  filter(lambda == min(lambda), term!= "(Intercept)") %>% 
  slice_max(order_by = abs(estimate), n = 50) %>% 
  mutate(term = fct_reorder(term, estimate)) %>% 
  ggplot(aes(estimate, term, fill = estimate > 0)) +
  geom_col(show.legend = F)+
  xlab("Coefficient in regularized linear model")

  • Name turned out to be important!

  • As longitude decreases price decreases? Hidden places fetch more price? 4 bedroom houses are pricier. Makes sense.

Stack a boosted tree model and a linear model

  • Model stacks can be thought of as a group of fitted member models and a set of instructions on how to combine their predictions.

  • Library stacks requires you to save predictions and model as we did in the control_grid

# Keep only the results from the numerically best combination
lin_best <- lin_tune %>% 
  filter_parameters(parameters = select_best(lin_tune))
xg_best <- xg_tune %>% 
  filter_parameters(parameters = select_best(xg_tune))

# Create a stack of models
lin_xg_stack <- stacks() %>%
  add_candidates(lin_best) %>% 
  add_candidates(xg_best)
  
lin_xg_stack

Under the hood, a data_stack object is really just a tibble with some extra attributes. Checking out the actual data:

lin_xg_stack %>% 
  as_tibble() %>% 
  slice_head(n=5)

The first column gives the first response value, and the remaining columns give the assessment set predictions for each ensemble member.

That’s it! We’re now ready to evaluate how it is that we need to combine predictions from each candidate ensemble member.

Fit the stack

The outputs from each of these candidate ensemble members are highly correlated, so the blend_predictions() function performs regularization to figure out how we can combine the outputs from the stack members to come up with a final prediction.

The blend_predictions function determines how member model output will ultimately be combined in the final prediction by fitting a LASSO model on the data stack, predicting the true assessment set outcome using the predictions from each of the candidate members. Candidates with nonzero stacking coefficients become members.

# Fit the stack
blended_lin_xg <- lin_xg_stack %>% 
  blend_predictions()

blended_lin_xg
## # A tibble: 2 x 3
##   member        type       weight
##   <chr>         <chr>       <dbl>
## 1 xg_best_1_124 boost_tree  0.573
## 2 lin_best_2_42 linear_reg  0.492

Now that we know how to combine our model output, we can fit the candidates with non-zero stacking coefficients on the full training set.

blended_lin_xg_fit <- blended_lin_xg %>% 
  fit_members()

blended_lin_xg_fit
## # A tibble: 2 x 3
##   member        type       weight
##   <chr>         <chr>       <dbl>
## 1 xg_best_1_124 boost_tree  0.573
## 2 lin_best_2_42 linear_reg  0.492
class(blended_lin_xg_fit)
## [1] "linear_stack" "model_stack"  "list"

The object is now ready to predict with new data

# Make predictions on test set
test_stacks <- test %>% 
  bind_cols(predict(blended_lin_xg_fit, .))

# Evaluate stack
test_stacks %>% 
  rmse(truth = price, estimate = .pred)

Yes! We have lowered rmse! Good job!

Little trick by David to get more power out of stacked models. Retrain the stacks on the whole data set

# Make a copy of blended stack
blended_lin_xg_fulldata <- blended_lin_xg

# Substitute the train component with the whole initial data
blended_lin_xg_fulldata$train <- play_data

# Train the stacks object on the whole data
blended_lin_xg_fulldata_fit <- blended_lin_xg_fulldata %>% 
  fit_members()

# Function to make augment() work with stacks
augment.model_stack <- function(stack_object, data){
  data %>% 
    bind_cols(predict(stack_object, .))
}

# Predict on holdout
blended_lin_xg_fulldata_fit %>% 
  predict_holdout() %>% 
  write_csv("lin_xg_stack_pred.csv")
LS0tDQp0aXRsZTogJ0ludGVybnNoaXAgTUwgUHJlcDogRGF0YSB3aXRoIHNwYXRpYWwgY29vcmRpbmF0ZXMgYW5kIHRleHQgZmVhdHVyZXMnDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgY3NzOiBzdHlsZV83LmNzcw0KICAgIGRmX3ByaW50OiBwYWdlZA0KICAgIHRoZW1lOiBmbGF0bHkNCiAgICBoaWdobGlnaHQ6IGJyZWV6ZWRhcmsNCiAgICB0b2M6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogICAgY29kZV9kb3dubG9hZDogeWVzDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQod2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UpDQoNCmBgYA0KDQpJbiB0aGlzIGVwaXNvZGUgb2YgU0xJQ0VELCBjb250ZXN0YW50cyBhcmUgY2hhbGxlbmdlZCB0byBwcmVkaWN0IHRoZSBwcmljZSBvZiBBaXJibmIgbGlzdGluZ3MgaW4gTllDLg0KDQpUaGXCoGV2YWx1YXRpb24gbWV0cmljIGZvciB0aGlzIGNvbXBldGl0aW9uIGlzIFtSb290IE1lYW4gU3F1YXJlZCBMb2dhcml0aG1pYyBFcnJvcl0oaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS93aWtpL1Jvb3RNZWFuU3F1YXJlZExvZ2FyaXRobWljRXJyb3IpLg0KDQojIyBTcGxpdCBEYXRhDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoMjAyMSkNCiMgTG9hZCByZXF1aXJlZCBsaWJyYXJpZXMNCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeSh0aWR5bW9kZWxzKQ0KbGlicmFyeShoZXJlKQ0KbGlicmFyeSh0ZXh0cmVjaXBlcykNCmxpYnJhcnkoc3RhY2tzKQ0KbGlicmFyeShicm9vbSkNCg0KIyBJbXBvcnQgZGF0YQ0KcGxheV9kYXRhIDwtIHJlYWRfY3N2KCJ0cmFpbi5jc3YiLCBzaG93X2NvbF90eXBlcyA9IEZBTFNFKSAlPiUgDQogICMgQWNjb3VudCBmb3IgMCBieSBhZGRpbmcgMSANCiAgbXV0YXRlKHByaWNlID0gbG9nKHByaWNlICsgMSkpDQoNCmhvbGRvdXRfZGF0YSA8LSByZWFkX2NzdigidGVzdC5jc3YiLCBzaG93X2NvbF90eXBlcyA9IEZBTFNFKQ0KDQojIEZ1bmN0aW9uIGZvciBwcmVkaWN0aW5nIG9uIHRoZSBob2xkIG91dCBkYXRhDQpwcmVkaWN0X2hvbGRvdXQgPC0gZnVuY3Rpb24od2Ypew0KICB3ZiAlPiUgYXVnbWVudChob2xkb3V0X2RhdGEpICU+JSANCiAgICBtdXRhdGUoLnByZWQgPSBleHAoLnByZWQpIC0gMSkgJT4lIA0KICAgIHNlbGVjdChpZCwgcHJpY2UgPSAucHJlZCkNCn0NCg0KDQojIEZ1bmN0aW9uIHRvIG1ha2UgYXVnbWVudCgpIHdvcmsgd2l0aCBzdGFja3MNCmF1Z21lbnQubW9kZWxfc3RhY2sgPC0gZnVuY3Rpb24oc3RhY2tfb2JqZWN0LCBkYXRhKXsNCiAgZGF0YSAlPiUgDQogICAgYmluZF9jb2xzKHByZWRpY3Qoc3RhY2tfb2JqZWN0LCAuKSkNCn0NCg0KDQojIFNwbGl0IGRhdGENCmRhdGFfc3BsaXQgPC0gaW5pdGlhbF9zcGxpdChwbGF5X2RhdGEsIHByb3AgPSAwLjc1KQ0KDQojIEV4dHJhY3QgZGF0YSBmcm9tIHNwbGl0IGRhdGENCnRyYWluIDwtIHRyYWluaW5nKGRhdGFfc3BsaXQpDQp0ZXN0IDwtIHRlc3RpbmcoZGF0YV9zcGxpdCkNCmBgYA0KDQojIyBFREENCg0KYGBge3J9DQojIEJyaWVmIHN1bW1hcnkgb2YgdGhlIHZhcmlhYmxlcw0KdHJhaW4gJT4lIA0KICBnbGltcHNlKCkNCmBgYA0KDQpEaXN0cmlidXRpb24gb2Ygd2hhdCB3ZSBhcmUgdHJ5aW5nIHRvIHByZWRpY3QNCg0KYGBge3J9DQp0aGVtZV9zZXQodGhlbWVfbGlnaHQoKSkNCnRyYWluICU+JSANCiAgIyBIYXMgMHMgaW4gaXQgc28gd2UgYWRkIDENCiAgZ2dwbG90KG1hcHBpbmcgPSBhZXMoeCA9IHByaWNlKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShmaWxsID0gIm1pZG5pZ2h0Ymx1ZSIsIGFscGhhID0gMC43KSANCg0KIyBGaW5kIG1lZGlhbiBhbmQgYXZlcmFnZSBwcmljZXMNCnN1bW1hcnlfcHJpY2UgPC0gZnVuY3Rpb24odGJsKXsNCiAgdGJsICU+JSANCiAgICBzdW1tYXJpemUoYXZnX3ByaWNlID0gZXhwKG1lYW4ocHJpY2UpKS0xLA0KICAgICAgICAgICAgICBuID0gbigpKSAlPiUgYXJyYW5nZShkZXNjKG4pKQ0KICAgICAgICAgICAgICANCn0NCg0KdHJhaW4gJT4lIA0KICBzdW1tYXJ5X3ByaWNlKCkNCmBgYA0KDQpgYGB7cn0NCiMgSG93IGF2ZXJhZ2UgcHJpY2UgdmFyaWVzIHBlciBuZWlnaGJvdXJob29kX2dyb3VwDQp0cmFpbiAlPiUgDQogIG11dGF0ZShuZWlnaGJvdXJob29kX2dyb3VwID0gZmN0X3Jlb3JkZXIobmVpZ2hib3VyaG9vZF9ncm91cCwgcHJpY2UpKSAlPiUgDQogIGdncGxvdChtYXBwaW5nID0gYWVzKHggPSBleHAocHJpY2UpLCB5ID0gbmVpZ2hib3VyaG9vZF9ncm91cCApKSArDQogIGdlb21fYm94cGxvdCgpICsNCiAgc2NhbGVfeF9sb2cxMCgpDQoNCiMgSG93IGF2ZXJhZ2UgcHJpY2UgdmFyaWVzIHBlciBuZWlnaGJvdXJob29kDQp0cmFpbiAlPiUgDQogIG11dGF0ZShuZWlnaGJvdXJob29kID0gZmN0X2x1bXAobmVpZ2hib3VyaG9vZCwgNDApLA0KICAgIG5laWdoYm91cmhvb2QgPSBmY3RfcmVvcmRlcihuZWlnaGJvdXJob29kLCBwcmljZSkpICU+JSANCiAgZ2dwbG90KG1hcHBpbmcgPSBhZXMoeCA9IGV4cChwcmljZSksIHkgPSBuZWlnaGJvdXJob29kICkpICsNCiAgZ2VvbV9ib3hwbG90KCkgKw0KICBzY2FsZV94X2xvZzEwKCkNCg0KIyBIb3cgcHJpY2UgdmFyaWVzIHdpdGggcm9vbSB0eXBlDQp0cmFpbiAlPiUgDQogIG11dGF0ZShyb29tX3R5cGUgPSBmY3RfcmVvcmRlcihyb29tX3R5cGUsIHByaWNlKSkgJT4lIA0KICBnZ3Bsb3QobWFwcGluZyA9IGFlcyh4ID0gZXhwKHByaWNlKSwgeSA9IHJvb21fdHlwZSApKSArDQogIGdlb21fYm94cGxvdCgpICsNCiAgc2NhbGVfeF9sb2cxMCgpDQoNCiMgSG93IHByaWNlIHZhcmllcyB3aXRoIHJldmlld3MgcGVyIG1vbnRoDQp0cmFpbiAlPiUgDQogIGdncGxvdChtYXBwaW5nID0gYWVzKHggPSBudW1iZXJfb2ZfcmV2aWV3cywgeSA9IHByaWNlICkpICsNCiAgZ2VvbV9wb2ludCgpICsNCiAgc2NhbGVfeF9sb2cxMCgpICsNCiAgZ2VvbV9zbW9vdGgobWV0aG9kID0gImxtIikNCg0KDQojIEhvdyBwcmljZSB2YXJpZXMgd2l0aCByZXZpZXdzIHBlciBtb250aA0KdHJhaW4gJT4lIA0KICBnZ3Bsb3QobWFwcGluZyA9IGFlcyh4ID0gcmV2aWV3c19wZXJfbW9udGgsIHkgPSBwcmljZSApKSArDQogIGdlb21fcG9pbnQoKSArDQogIHNjYWxlX3hfbG9nMTAoKSArDQogIGdlb21fc21vb3RoKG1ldGhvZCA9ICJsbSIpDQoNCiMgSG93IHByaWNlIHZhcmllcyB3aXRoIGxhc3QgcmV2aWV3DQp0cmFpbiAlPiUgDQogIGdncGxvdChtYXBwaW5nID0gYWVzKHggPSBsYXN0X3JldmlldywgeSA9IHByaWNlICkpICsNCiAgZ2VvbV9wb2ludCgpICsNCiAgZ2VvbV9zbW9vdGgobWV0aG9kID0gImxtIikNCg0KIyBIb3cgcHJpY2UgdmFyaWVzIHdpdGggbnVtYmVyIG9mIG1pbmltdW0gbmlnaHRzDQp0cmFpbiAlPiUgDQogIHNsaWNlX3NhbXBsZShuID0gMTAwMCkgJT4lIA0KICBnZ3Bsb3QobWFwcGluZyA9IGFlcyh4ID0gbWluaW11bV9uaWdodHMsIHkgPSBwcmljZSApKSArDQogIGdlb21fcG9pbnQoKSArDQogIHNjYWxlX3hfbG9nMTAoKSArDQogIGdlb21fc21vb3RoKG1ldGhvZCA9ICJsbSIpDQpgYGANCg0KIyMgVGV4dCBBbmFseXNpcw0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXRleHQpDQoNCiMgV29yZCB0b2tlbml6YXRpb24gdG8gc2VlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIHdvcmQgYW5kIHByaWNlDQp0cmFpbiAlPiUgDQogIHVubmVzdF90b2tlbnMoaW5wdXQgPSBuYW1lLCBvdXRwdXQgPSB3b3JkLCB0b2tlbiA9ICJ3b3JkcyIpICU+JSANCiAgZ3JvdXBfYnkod29yZCkgJT4lIA0KICBzdW1tYXJ5X3ByaWNlKCkgJT4lIA0KICBoZWFkKDUwKSAlPiUgDQogIG11dGF0ZSh3b3JkID0gZmN0X3Jlb3JkZXIod29yZCwgYXZnX3ByaWNlKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBhdmdfcHJpY2UsIHkgPSB3b3JkLCBzaXplID0gbikpICsNCiAgZ2VvbV9wb2ludCgpDQoNCmBgYA0KDQojIyBTaW1wbGUgbWFwDQoNCmBgYHtyfQ0KbGlicmFyeShnZ21hcCkNCmxpYnJhcnkoZ2d0aGVtZXMpDQoNCiMgQWdncmVnYXRlIG5lYXJieSBhcmVhcw0KYWdnX2xhdF9sb25nIDwtIHRyYWluICU+JSANCiAgZ3JvdXBfYnkobGF0aXR1ZGUgPSByb3VuZChsYXRpdHVkZSwgMiksDQogICAgICAgICAgIGxvbmdpdHVkZSA9IHJvdW5kKGxvbmdpdHVkZSwgMikpICU+JSANCiAgc3VtbWFyaXplKHByaWNlID0gbWVhbihwcmljZSksIG4gPSBuKCkpICU+JSANCiAgZmlsdGVyKG4gPj0gNSkNCg0KIyBGaW5kIHJhbmdlIG9mIGNvdmVyYWdlDQpyYW5nZSh0cmFpbiAlPiUgcHVsbChsYXRpdHVkZSkpDQpyYW5nZSh0cmFpbiAlPiUgcHVsbChsb25naXR1ZGUpKQ0KDQpiYm94IDwtIGMobGVmdCA9IC03NC4yNDI4NSwgIGJvdHRvbSA9IDQwLjUwNjQxLCByaWdodCA9IC03My43MTY5MCwgdG9wID0gNDAuOTEzMDYgKQ0KbnljX21hcCA8LSBnZXRfc3RhbWVubWFwKGJib3gsIHpvb20gPSAxMSkNCg0KIyBXaGljaCBhcmVhcyBoYXZlIGhpZ2ggcHJpY2VzDQpnZ21hcChueWNfbWFwKSArDQogIGdlb21fcG9pbnQoZGF0YSA9IGFnZ19sYXRfbG9uZywgYWVzKGxvbmdpdHVkZSwgbGF0aXR1ZGUsIGNvbG9yID0gZXhwKHByaWNlKSAtIDEsIHNpemUgPSBuKSkgKw0KICBzY2FsZV9jb2xvcl9ncmFkaWVudDIobG93ID0gImJsdWUiLCBoaWdoID0gInJlZCIsIG1pZHBvaW50ID0gMiwgdHJhbnMgPSAibG9nMTAiKSArDQogIHRoZW1lX21hcCgpICsNCiAgbGFicyhjb2xvciA9ICJQcmljZSIpDQpgYGANCg0KLSAgIFByZXNlbmNlIG9mIGNlcnRhaW4gd29yZHMgcmVhbGx5IGRvZXMgbWF0dGVyIGluIHRlcm1zIG9mIHByaWNpbmcNCg0KLSAgIEFyZWFzIGluIGNlcnRhaW4gbG9uZyBhbmQgbGF0IGhhdmUgZGlzdGluY3RpdmUgaGlnaCBwcmljZXMNCg0KLSAgIFNlZW1zIHRoZXJlIGlzIGEgZGlzcGFyaXR5IG9mIHByaWNlcyBkZXBlbmRpbmcgb24gdGhlIG5laWdoYm91cmhvb2QgZ3JvdXAgYW5kIG5laWdoYm91cmhvb2QNCg0KLSAgIFRoZSByb29tIHR5cGUgY2xlYXJseSBwYXJ0aXRpb25zIHRoZSBob3VzZSBwcmljZXMNCg0KLSAgIFRoZSBvdGhlciBmZWF0dXJlcyBkb24ndCBzZWVtIHRvIGluZmx1ZW5jZSB0aGUgcHJpY2Ugc28gbXVjaCA/DQoNCiMjIFRyYWluIHhnYm9vc3QgbW9kZWwNCg0KQ3JlYXRlcyBhIHNlcmllcyBvZiBkZWNpc2lvbiB0cmVlcyBmb3JtaW5nIGFuIGVuc2VtYmxlLiBFYWNoIHRyZWUgZGVwZW5kcyBvbiB0aGUgcmVzdWx0cyBvZiBwcmV2aW91cyB0cmVlcyBpbiBhbiBhdHRlbXB0IHRvIHJlZHVjZSB0aGUgZXJyb3IuIEFsbCB0cmVlcyBpbiB0aGUgZW5zZW1ibGUgYXJlIGNvbWJpbmVkIHRvIHByb2R1Y2UgYSBmaW5hbCBwcmVkaWN0aW9uLg0KDQpgYGB7cn0NCiMgQ29udHJvbCBhc3BlY3RzIG9mIGEgZ3JpZCBzZWFyY2gNCmV2YWxfbWV0IDwtIG1ldHJpY19zZXQocm1zZSkNCmdyaWRfY29udHJvbCA8LSBjb250cm9sX2dyaWQoDQogICMgU2F2ZSBwcmVkaWN0aW9ucyBmb3IgZWFjaCBtb2RlbCBldmFsdWF0ZWQNCiAgc2F2ZV9wcmVkID0gVFJVRSwNCiAgIyBTYXZlIHdvcmtmbG93IChtb2RlbCtyZWNpcGUpDQogIHNhdmVfd29ya2Zsb3cgPSBUUlVFLA0KICAjIEV4dHJhY3QgYSBmaXR0ZWQgbW9kZWwNCiAgZXh0cmFjdCA9IGV4dHJhY3RfbW9kZWwpDQoNCiMgUmVzYW1wbGluZyB0ZWNobmlxdWVzIGZvciBjcmVhdGluZyBzaW11bGF0ZWQgZGF0YSBzZXRzDQpzZXQuc2VlZCgyMDIxKQ0KdHJhaW5fNWZvbGRfY3YgPC0gdHJhaW4gJT4lIA0KICB2Zm9sZF9jdih2ID0gNSwgcmVwZWF0cyA9IDEpDQp0cmFpbl81Zm9sZF9jdg0KYGBgDQoNCi0gICBEb2VzIHhnYm9vc3QgcmVxdWlyZSB1cyB0byBub3JtYWxpemUgb3IgdHJhbnNmb3JtIHZhcmlhYmxlcyB0byBiZSBtb3JlIHN5bW1ldHJpYz8gTm90IHJlYWxseS4gTW9yZSByZXNlYXJjaCBvbiB0aGlzDQoNCi0gICBXaGlsZSB0cmVlLWJhc2VkIGJvb3N0aW5nIG1ldGhvZHMgZ2VuZXJhbGx5IGRvIG5vdCByZXF1aXJlIHRoZSBjcmVhdGlvbiBvZiBkdW1teSB2YXJpYWJsZXMsIG1vZGVscyB1c2luZyB0aGUgYHhnYm9vc3RgIGVuZ2luZSBkby4NCg0KYGBge3J9DQojIEZ1bmN0aW9uIHRoYXQgYXBwbGllcyBwcmVwcm9jZXNzaW5nIHN0ZXBzIHRvIGRhdGEgKHNhbml0eSBjaGVjaykNCnByZXBfYmFrZSA8LSBmdW5jdGlvbihyZWNpcGUpIGp1aWNlKHByZXAocmVjaXBlKSkNCg0KIyBEYXRhIHByZXByb2Nlc3Npbmcgd2l0aCByZWNpcGVzIC0gTU9TVExZIE5VTUVSSUMNCnhnX3JlYyA8LSByZWNpcGUocHJpY2UgfiBtaW5pbXVtX25pZ2h0cyArIHJvb21fdHlwZSArIG51bWJlcl9vZl9yZXZpZXdzICsgbGF0aXR1ZGUgKyBsb25naXR1ZGUgKyBuZWlnaGJvdXJob29kX2dyb3VwICsgcmV2aWV3c19wZXJfbW9udGggKyBjYWxjdWxhdGVkX2hvc3RfbGlzdGluZ3NfY291bnQgKyBhdmFpbGFiaWxpdHlfMzY1ICsgbGFzdF9yZXZpZXcsIGRhdGEgPSB0cmFpbikgJT4lIA0KICAjIE1ha2UgdmFyaWFibGVzIHN5bW1ldHJpY2FsDQogICNzdGVwX2xvZyhhbGxfbm9taW5hbF9wcmVkaWN0b3JzKCksIG9mZnNldCA9IDEpICU+JSANCiAgIyBpZiBub3QgZXZlciByZXZpZXdlZCwgMA0KICAjIE5hcnJvdyBkb3duIHRvIE1hbmhhdHRhbiBhcyBzdWdnZXN0ZWQgYnkgdGhlIFZJUCBwbG90IGJlbG93DQogIHN0ZXBfbXV0YXRlKG1hbmhhdHRhbl9ncnAgPSBuZWlnaGJvdXJob29kX2dyb3VwID09ICJNYW5oYXR0YW4iKSAlPiUgDQogIHN0ZXBfcm0obmVpZ2hib3VyaG9vZF9ncm91cCkgJT4lIA0KICBzdGVwX211dGF0ZShsYXN0X3JldmlldyA9IGNvYWxlc2NlKGFzLmludGVnZXIoU3lzLkRhdGUoKSAtIGxhc3RfcmV2aWV3KSwgMCkpICU+JSANCiAgc3RlcF9kdW1teShhbGxfbm9taW5hbF9wcmVkaWN0b3JzKCkpDQoNCiMgU2FuaXR5IGNoZWNrDQojIHhnX3JlYyAlPiUgDQojICAgcHJlcF9iYWtlKCkgJT4lIA0KIyAgIHNsaWNlX2hlYWQobiA9IDUpDQoNCiMgTWFrZSBhIG1vZGVsIHNwZWNpZmljYXRpb24NCnhnX21vZCA8LSBib29zdF90cmVlKA0KICAjIFJhbmRvbWx5IHNlbGVjdGVkIHByZWRpY3RvcnMgYXQgZWFjaCBzcGxpdA0KICBtdHJ5ID0gdHVuZSgpLA0KICAjIE51bWJlciBvZiB0cmVlcw0KICB0cmVlcyA9IHR1bmUoKSwNCiAgbGVhcm5fcmF0ZSA9IDAuMDENCikgJT4lIA0KICBzZXRfZW5naW5lKCJ4Z2Jvb3N0IikgJT4lIA0KICBzZXRfbW9kZSgicmVncmVzc2lvbiIpDQoNCiMgQ29tYmluZSBtb2RlbCBzcGVjaWZjYXRpb24gYW5kIHJlY2lwZSBpbnRvIGEgd29ya2Zsb3cNCnhnX3dmIDwtIHdvcmtmbG93KCkgJT4lIA0KICBhZGRfcmVjaXBlKHhnX3JlYykgJT4lIA0KICBhZGRfbW9kZWwoeGdfbW9kKQ0KDQojIFR1bmUgbW9kZWwgaHlwZXJwYXJhbWV0ZXJzDQpkb1BhcmFsbGVsOjpyZWdpc3RlckRvUGFyYWxsZWwoKQ0KeGdfdHVuZSA8LSB4Z193ZiAlPiUgDQogIHR1bmVfZ3JpZCgNCiAgICByZXNhbXBsZXMgPSB0cmFpbl81Zm9sZF9jdiwNCiAgICBtZXRyaWNzID0gZXZhbF9tZXQsDQogICAgY29udHJvbCA9IGdyaWRfY29udHJvbCwNCiAgICBncmlkID0gY3Jvc3NpbmcobXRyeSA9IGMoNCwgNSwgNiwgIDcpLCB0cmVlcyA9IHNlcSgyNTAsIDEwMDAsIDI1KSkNCiAgKQ0KDQp4Z190dW5lICU+JSBhdXRvcGxvdCgpDQoNCiMgU2VlIHBlcmZvcm1hbmNlIG1ldHJpY3MNCnhnX3R1bmUgJT4lIA0KICBjb2xsZWN0X21ldHJpY3MoKSAlPiUgDQogIGFycmFuZ2UoKG1lYW4pKQ0KDQpgYGANCg0KLSAgIFJNU0Ugb2YgMC41MzUgd2l0aCA2MDAgdHJlZXMgYW5kIG10cnkgPSAyOiBwcmljZSBcfiBtaW5pbXVtX25pZ2h0cyArIHJvb21fdHlwZSArIG51bWJlcl9vZl9yZXZpZXdzDQoNCi0gICBSTVNFIG9mIDAuNDY4IHdpdGggMTAwMCB0cmVlcyBhbmQgbXRyeSA9IDY6IHByaWNlIFx+IG1pbmltdW1fbmlnaHRzICsgcm9vbV90eXBlICsgbnVtYmVyX29mX3Jldmlld3MgKyBsYXRpdHVkZSArIGxvbmdpdHVkZSArIG5laWdoYm91cmhvb2RfZ3JvdXAgLSBkbyB3ZSBuZWVkIDEwMDAgdHJlZXM/IE92ZXJmaXR0aW5nPz8NCg0KLSAgIFJNU0Ugb2YgMC40NDIgd2l0aCA4MDAgdHJlZXMgYW5kIG10cnkgPSA2OiBwcmljZSBcfiBtaW5pbXVtX25pZ2h0cyArIHJvb21fdHlwZSArIG51bWJlcl9vZl9yZXZpZXdzICsgbGF0aXR1ZGUgKyBsb25naXR1ZGUgKyBuZWlnaGJvdXJob29kX2dyb3VwICsgcmV2aWV3c19wZXJfbW9udGggKyBjYWxjdWxhdGVkX2hvc3RfbGlzdGluZ3NfY291bnQgKyBhdmFpbGFiaWxpdHlfMzY1DQoNCi0gICBSTVNFIG9mIDAuNDQxOCB3aXRoIDgwMCB0cmVlcyBhbmQgbXRyeSA9IDc6IHByaWNlIFx+IG1pbmltdW1fbmlnaHRzICsgcm9vbV90eXBlICsgbnVtYmVyX29mX3Jldmlld3MgKyBsYXRpdHVkZSArIGxvbmdpdHVkZSArIG5laWdoYm91cmhvb2RfZ3JvdXAgKyByZXZpZXdzX3Blcl9tb250aCArIGNhbGN1bGF0ZWRfaG9zdF9saXN0aW5nc19jb3VudCArIGF2YWlsYWJpbGl0eV8zNjUgKyBsYXN0X3JldmlldyAtLS0gRmluYWxpemUgdGhpcw0KDQotICAgUk1TRSBvZiAwLjQ0MTMgd2l0aCAxMDAwIHRyZWVzIGFuZCBtdHJ5ID0gNzogcHJpY2UgXH4gbWluaW11bV9uaWdodHMgKyByb29tX3R5cGUgKyBudW1iZXJfb2ZfcmV2aWV3cyArIGxhdGl0dWRlICsgbG9uZ2l0dWRlICsgbmVpZ2hib3VyaG9vZF9ncm91cCArIHJldmlld3NfcGVyX21vbnRoICsgY2FsY3VsYXRlZF9ob3N0X2xpc3RpbmdzX2NvdW50ICsgYXZhaWxhYmlsaXR5XzM2NSArIGxhc3RfcmV2aWV3IC0tLSB3aXRoIG1hbmhhdHRhbiBhcyBhIGZlYXR1cmUNCg0KRmluYWxpemUgdmVyc2lvbiB3aXRob3V0IG1hbnkgY2F0ZWdvcmljYWwgdmFsdWVzDQoNCmBgYHtyfQ0KIyBGaW5hbGl6ZSB3b3JrZmxvdyBhbmQgZml0IG1vZGVsDQp4Z19maXQgPC0geGdfd2YgJT4lIA0KICBmaW5hbGl6ZV93b3JrZmxvdyhzZWxlY3RfYmVzdCh4Z190dW5lKSkgJT4lIA0KICBmaXQodHJhaW4pDQoNCiMgRXZhbHVhdGUgbW9kZWwgcGVyZm9ybWFuY2Ugb24gdGVzdCBzZXQNCnhnX2ZpdCAlPiUgDQogIGF1Z21lbnQodGVzdCkgJT4lIA0KICBybXNlKHRydXRoID0gcHJpY2UsIGVzdGltYXRlID0gLnByZWQpDQoNCiMgVmFyaWFibGUgaW1wb3J0YW5jZSBwbG90DQppbXBvcnRhbnRfdmFyIDwtIHhnYm9vc3Q6OnhnYi5pbXBvcnRhbmNlKG1vZGVsID0geGdfZml0JGZpdCRmaXQkZml0KQ0KDQppbXBvcnRhbnRfdmFyICU+JSANCiAgbXV0YXRlKEZlYXR1cmUgPSBmY3RfcmVvcmRlcihGZWF0dXJlLCBHYWluKSkgJT4lIA0KICBnZ3Bsb3QobWFwcGluZyA9IGFlcyh4ID0gR2FpbiwgeSA9IEZlYXR1cmUpKSArDQogIGdlb21fY29sKCkNCg0KIyBNYWtlIHByZWRpY3Rpb25zIG9uIGhvbGRvdXQgZGF0YQ0KeGdfZml0ICU+JSANCiAgcHJlZGljdF9ob2xkb3V0KCkgJT4lIA0KICB3cml0ZV9jc3YoInhnX251bWVyaWMiKQ0KDQpgYGANCg0KVGhlIE1hbmhhdHRhbiBuZWlnaGJvdXJob29kIGdyb3VwIHN0YW5kcyBvdXQgZnJvbSB0aGUgcmVzdCBvZiBpdHMgcGVlcnMuDQoNCiMjIExpbmVhciB2ZXJzaW9uIG9uIHRoZSBjYXRlZ29yaWNhbA0KDQotICAgQWRkaXRpb25hbGx5LCBgc3RlcF9vdGhlcigpYCBjYW4gYmUgdXNlZCB0byBhbmFseXplIHRoZSBmcmVxdWVuY2llcyBvZiB0aGUgZmFjdG9yIGxldmVscyBpbiB0aGUgdHJhaW5pbmcgc2V0IGFuZCBjb252ZXJ0IGluZnJlcXVlbnRseSBvY2N1cnJpbmcgdmFsdWVzIHRvIGEgY2F0Y2gtYWxsIGxldmVsIG9mICJvdGhlciIsIHdpdGggYSBzcGVjaWZpYyB0aHJlc2hvbGQgdGhhdCBjYW4gYmUgc3BlY2lmaWVkLg0KDQogICAgTHVtcHMgaW5mcmVxdWVudGx5IG9jY3VycmluZyBvYnNlcnZhdGlvbnMgaW50byBhIG5ldyBsZXZlbCBjYWxsZWQgb3RoZXIuDQoNCi0gICBOb3JtYWxpemUgdG8gY29tcGFyZSB0aGVtIG9uIHRoZSBzYW1lIHNjYWxlIGxhdGVyDQoNCmBgYHtyfQ0KIyBSZWNpcGUgZm9yIHByZXByb2Nlc3NpbmcgZGF0YQ0KbGluX3JlYyA8LSByZWNpcGUocHJpY2UgfiBuYW1lICsgcm9vbV90eXBlICsNCiAgICAgICAgICAgICAgICAgICAgbGF0aXR1ZGUgKyBsb25naXR1ZGUgKw0KICAgICAgICAgICAgICAgICAgICBuZWlnaGJvdXJob29kX2dyb3VwICsgbmVpZ2hib3VyaG9vZCArDQogICAgICAgICAgICAgICAgICAgIGhvc3RfaWQsIGRhdGEgPSB0cmFpbikgJT4lIA0KICAjIFRva2VuaXplIG5hbWVzDQogIHN0ZXBfdG9rZW5pemUobmFtZSkgJT4lIA0KICAjIE5hcnJvdyBkb3duIHRvIHRvcCAxMDAgd29yZHM/DQogIHN0ZXBfdG9rZW5maWx0ZXIobmFtZSwgbWF4X3Rva2VucyA9IHR1bmUoKSkgJT4lIA0KICAjIEZpbmQgaWYgYSB0b2tlbiBpcyBmb3VuZCBpbiB0aGUgY29sdW1uIC0gZHVtbWluaXNlIA0KICBzdGVwX3RmKG5hbWUpICU+JSANCiAgc3RlcF9tdXRhdGUoaG9zdF9pZCA9IGZhY3Rvcihob3N0X2lkKSkgJT4lIA0KICBzdGVwX290aGVyKGhvc3RfaWQsIG5laWdoYm91cmhvb2QsIHRocmVzaG9sZCA9IHR1bmUoKSkgJT4lIA0KICBzdGVwX2R1bW15KGFsbF9ub21pbmFsX3ByZWRpY3RvcnMoKSkNCg0KIyBMaW5lYXIgbW9kZWwgc3BlY2lmaWNhdGlvbg0KbGluX21vZCA8LSBsaW5lYXJfcmVnKHBlbmFsdHkgPSB0dW5lKCkpICU+JSANCiAgc2V0X2VuZ2luZSgiZ2xtbmV0IikgJT4lIA0KICBzZXRfbW9kZSgicmVncmVzc2lvbiIpDQoNCiMgQnVuZGxlIG1vZGVsIHNwZWMgKyByZWNpcGUgaW50byB3b3JrZmxvdw0KbGluX3dmIDwtIHdvcmtmbG93KCkgJT4lIA0KICBhZGRfcmVjaXBlKGxpbl9yZWMpICU+JSANCiAgYWRkX21vZGVsKGxpbl9tb2QpDQoNCiMgTW9kZWwgdHVuaW5nDQojIFR1bmUgbW9kZWwgaHlwZXJwYXJhbWV0ZXJzDQpkb1BhcmFsbGVsOjpyZWdpc3RlckRvUGFyYWxsZWwoKQ0KbGluX3R1bmUgPC0gbGluX3dmICU+JSANCiAgdHVuZV9ncmlkKA0KICAgIHJlc2FtcGxlcyA9IHRyYWluXzVmb2xkX2N2LA0KICAgIG1ldHJpY3MgPSBldmFsX21ldCwNCiAgICBjb250cm9sID0gZ3JpZF9jb250cm9sLA0KICAgIGdyaWQgPSBjcm9zc2luZyhwZW5hbHR5ID0gMTAgXiBzZXEoLTcsIC0xLCAuMSksDQogICAgICAgICAgICAgICAgICAgIHRocmVzaG9sZCA9IGMoMC4wMDA4LDAuMDAxKSwNCiAgICAgICAgICAgICAgICAgICAgbWF4X3Rva2VucyA9IGMoMTAwLCA1MDApKSkNCg0KYXV0b3Bsb3QobGluX3R1bmUpDQoNCiMgQ29sbGVjdCBtZXRyaWNzDQpsaW5fdHVuZSAlPiUgDQogIGNvbGxlY3RfbWV0cmljcygpICU+JSANCiAgYXJyYW5nZShtZWFuKQ0KYGBgDQoNCi0gICBBYm91dCAwLjQ4IG9uIHJvb21fdHlwZSArIGxhdGl0dWRlICsgbG9uZ2l0dWRlICsgbmVpZ2hib3VyaG9vZF9ncm91cCArIG5laWdoYm91cmhvb2QgKyBob3N0X2lkIC4gU2VlbXMgbG93ZXIgdGhyZXNob2xkKHN0ZXBfb3RoZXIpIGRvZXMgYmV0dGVyDQoNCi0gICBCcmluZ2luZyBpbiB0b3AgMTAwIHdvcmRzIGltcHJvdmVkIHBlcmZvcm1hbmNlLiBUcnlpbmcgdG8gdHVuZSB0aGUgbWF4X3Rva2Vucy4gU2V0dGxlIGZvciBhIHRocmVzaG9sZCBvZiAwLjAwMSAoMC4xJT8pIFJlc3VsdGVkIGluICoqMC40NSoqDQoNCmBgYHtyfQ0KIyBGaW5hbGl6ZSB3b3JrbG93IGFuZCB0cmFpbiBtb2RlbA0KbGluX2ZpdCA8LSBsaW5fd2YgJT4lIA0KICBmaW5hbGl6ZV93b3JrZmxvdyhzZWxlY3RfYmVzdChsaW5fdHVuZSkpICU+JSANCiAgZml0KHRyYWluKQ0KDQojIE1ha2UgcHJlZGljdGlvbnMgb24gdGVzdCBzZXQNCmxpbl9maXQgJT4lIA0KICBhdWdtZW50KHRlc3QpICU+JSANCiAgcm1zZSh0cnV0aCA9IHByaWNlLCBlc3RpbWF0ZSA9IC5wcmVkKQ0KYGBgDQoNCldvdyEgQWxtb3N0IGFzIGdvb2QgYXMgeGdfYm9vc3QgLS0gKiowLjQzNyoqDQoNCk5vdywgbGV0J3MgaW50ZXJwcmV0IHRoZSBtb2RlbCBhbmQgc2VlIHdoaWNoIHZhcmlhYmxlcyBjb250cmlidXRlZCB0byBwcmVkaWN0aW9uDQoNCmBgYHtyfQ0KIyBJbnRlcnByZXQgbW9kZWwNCmxpYnJhcnkoYnJvb20pDQoNCmxpbl9maXQkZml0JGZpdCRmaXQgJT4lIA0KICB0aWR5KCkgJT4lIA0KICBmaWx0ZXIobGFtYmRhID49IHNlbGVjdF9iZXN0KGxpbl90dW5lKSRwZW5hbHR5KSAlPiUgDQogIGZpbHRlcihsYW1iZGEgPT0gbWluKGxhbWJkYSksIHRlcm0hPSAiKEludGVyY2VwdCkiKSAlPiUgDQogIHNsaWNlX21heChvcmRlcl9ieSA9IGFicyhlc3RpbWF0ZSksIG4gPSA1MCkgJT4lIA0KICBtdXRhdGUodGVybSA9IGZjdF9yZW9yZGVyKHRlcm0sIGVzdGltYXRlKSkgJT4lIA0KICBnZ3Bsb3QoYWVzKGVzdGltYXRlLCB0ZXJtLCBmaWxsID0gZXN0aW1hdGUgPiAwKSkgKw0KICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEYpKw0KICB4bGFiKCJDb2VmZmljaWVudCBpbiByZWd1bGFyaXplZCBsaW5lYXIgbW9kZWwiKQ0KDQpgYGANCg0KLSAgIE5hbWUgdHVybmVkIG91dCB0byBiZSBpbXBvcnRhbnQhDQoNCi0gICBBcyBsb25naXR1ZGUgZGVjcmVhc2VzIHByaWNlIGRlY3JlYXNlcz8gSGlkZGVuIHBsYWNlcyBmZXRjaCBtb3JlIHByaWNlPyA0IGJlZHJvb20gaG91c2VzIGFyZSBwcmljaWVyLiBNYWtlcyBzZW5zZS4NCg0KIyMgU3RhY2sgYSBib29zdGVkIHRyZWUgbW9kZWwgYW5kIGEgbGluZWFyIG1vZGVsDQoNCi0gICBNb2RlbCBzdGFja3MgY2FuIGJlIHRob3VnaHQgb2YgYXMgYSBncm91cCBvZiBmaXR0ZWQgbWVtYmVyIG1vZGVscyBhbmQgYSBzZXQgb2YgaW5zdHJ1Y3Rpb25zIG9uIGhvdyB0byBjb21iaW5lIHRoZWlyIHByZWRpY3Rpb25zLg0KDQotICAgTGlicmFyeSBzdGFja3MgcmVxdWlyZXMgeW91IHRvIHNhdmUgcHJlZGljdGlvbnMgYW5kIG1vZGVsIGFzIHdlIGRpZCBpbiB0aGUgY29udHJvbF9ncmlkDQoNCmBgYHtyfQ0KIyBLZWVwIG9ubHkgdGhlIHJlc3VsdHMgZnJvbSB0aGUgbnVtZXJpY2FsbHkgYmVzdCBjb21iaW5hdGlvbg0KbGluX2Jlc3QgPC0gbGluX3R1bmUgJT4lIA0KICBmaWx0ZXJfcGFyYW1ldGVycyhwYXJhbWV0ZXJzID0gc2VsZWN0X2Jlc3QobGluX3R1bmUpKQ0KeGdfYmVzdCA8LSB4Z190dW5lICU+JSANCiAgZmlsdGVyX3BhcmFtZXRlcnMocGFyYW1ldGVycyA9IHNlbGVjdF9iZXN0KHhnX3R1bmUpKQ0KDQojIENyZWF0ZSBhIHN0YWNrIG9mIG1vZGVscw0KbGluX3hnX3N0YWNrIDwtIHN0YWNrcygpICU+JQ0KICBhZGRfY2FuZGlkYXRlcyhsaW5fYmVzdCkgJT4lIA0KICBhZGRfY2FuZGlkYXRlcyh4Z19iZXN0KQ0KICANCmxpbl94Z19zdGFjaw0KYGBgDQoNClVuZGVyIHRoZSBob29kLCBhIGBkYXRhX3N0YWNrYCBvYmplY3QgaXMgcmVhbGx5IGp1c3QgYSB0aWJibGUgd2l0aCBzb21lIGV4dHJhIGF0dHJpYnV0ZXMuIENoZWNraW5nIG91dCB0aGUgYWN0dWFsIGRhdGE6DQoNCmBgYHtyfQ0KbGluX3hnX3N0YWNrICU+JSANCiAgYXNfdGliYmxlKCkgJT4lIA0KICBzbGljZV9oZWFkKG49NSkNCmBgYA0KDQpUaGUgZmlyc3QgY29sdW1uIGdpdmVzIHRoZSBmaXJzdCByZXNwb25zZSB2YWx1ZSwgYW5kIHRoZSByZW1haW5pbmcgY29sdW1ucyBnaXZlIHRoZSBhc3Nlc3NtZW50IHNldCBwcmVkaWN0aW9ucyBmb3IgZWFjaCBlbnNlbWJsZSBtZW1iZXIuDQoNClRoYXQncyBpdCEgV2UncmUgbm93IHJlYWR5IHRvIGV2YWx1YXRlIGhvdyBpdCBpcyB0aGF0IHdlIG5lZWQgdG8gY29tYmluZSBwcmVkaWN0aW9ucyBmcm9tIGVhY2ggY2FuZGlkYXRlIGVuc2VtYmxlIG1lbWJlci4NCg0KIyMjIEZpdCB0aGUgc3RhY2sNCg0KVGhlIG91dHB1dHMgZnJvbSBlYWNoIG9mIHRoZXNlIGNhbmRpZGF0ZSBlbnNlbWJsZSBtZW1iZXJzIGFyZSBoaWdobHkgY29ycmVsYXRlZCwgc28gdGhlIFtgYmxlbmRfcHJlZGljdGlvbnMoKWBdKGh0dHBzOi8vc3RhY2tzLnRpZHltb2RlbHMub3JnL3JlZmVyZW5jZS9ibGVuZF9wcmVkaWN0aW9ucy5odG1sKSBmdW5jdGlvbiBwZXJmb3JtcyByZWd1bGFyaXphdGlvbiB0byBmaWd1cmUgb3V0IGhvdyB3ZSBjYW4gY29tYmluZSB0aGUgb3V0cHV0cyBmcm9tIHRoZSBzdGFjayBtZW1iZXJzIHRvIGNvbWUgdXAgd2l0aCBhIGZpbmFsIHByZWRpY3Rpb24uDQoNClRoZSBgYmxlbmRfcHJlZGljdGlvbnNgIGZ1bmN0aW9uIGRldGVybWluZXMgaG93IG1lbWJlciBtb2RlbCBvdXRwdXQgd2lsbCB1bHRpbWF0ZWx5IGJlIGNvbWJpbmVkIGluIHRoZSBmaW5hbCBwcmVkaWN0aW9uIGJ5IGZpdHRpbmcgYSBMQVNTTyBtb2RlbCBvbiB0aGUgZGF0YSBzdGFjaywgcHJlZGljdGluZyB0aGUgdHJ1ZSBhc3Nlc3NtZW50IHNldCBvdXRjb21lIHVzaW5nIHRoZSBwcmVkaWN0aW9ucyBmcm9tIGVhY2ggb2YgdGhlIGNhbmRpZGF0ZSBtZW1iZXJzLiBDYW5kaWRhdGVzIHdpdGggbm9uemVybyBzdGFja2luZyBjb2VmZmljaWVudHMgYmVjb21lIG1lbWJlcnMuDQoNCmBgYHtyfQ0KIyBGaXQgdGhlIHN0YWNrDQpibGVuZGVkX2xpbl94ZyA8LSBsaW5feGdfc3RhY2sgJT4lIA0KICBibGVuZF9wcmVkaWN0aW9ucygpDQoNCmJsZW5kZWRfbGluX3hnDQpgYGANCg0KTm93IHRoYXQgd2Uga25vdyBob3cgdG8gY29tYmluZSBvdXIgbW9kZWwgb3V0cHV0LCB3ZSBjYW4gZml0IHRoZSBjYW5kaWRhdGVzIHdpdGggbm9uLXplcm8gc3RhY2tpbmcgY29lZmZpY2llbnRzIG9uIHRoZSBmdWxsIHRyYWluaW5nIHNldC4NCg0KYGBge3J9DQpibGVuZGVkX2xpbl94Z19maXQgPC0gYmxlbmRlZF9saW5feGcgJT4lIA0KICBmaXRfbWVtYmVycygpDQoNCmJsZW5kZWRfbGluX3hnX2ZpdA0KDQpjbGFzcyhibGVuZGVkX2xpbl94Z19maXQpDQpgYGANCg0KVGhlIG9iamVjdCBpcyBub3cgcmVhZHkgdG8gcHJlZGljdCB3aXRoIG5ldyBkYXRhDQoNCmBgYHtyfQ0KIyBNYWtlIHByZWRpY3Rpb25zIG9uIHRlc3Qgc2V0DQp0ZXN0X3N0YWNrcyA8LSB0ZXN0ICU+JSANCiAgYmluZF9jb2xzKHByZWRpY3QoYmxlbmRlZF9saW5feGdfZml0LCAuKSkNCg0KIyBFdmFsdWF0ZSBzdGFjaw0KdGVzdF9zdGFja3MgJT4lIA0KICBybXNlKHRydXRoID0gcHJpY2UsIGVzdGltYXRlID0gLnByZWQpDQpgYGANCg0KWWVzISBXZSBoYXZlIGxvd2VyZWQgcm1zZSEgR29vZCBqb2IhDQoNCkxpdHRsZSB0cmljayBieSBEYXZpZCB0byBnZXQgbW9yZSBwb3dlciBvdXQgb2Ygc3RhY2tlZCBtb2RlbHMuIFJldHJhaW4gdGhlIHN0YWNrcyBvbiB0aGUgd2hvbGUgZGF0YSBzZXQNCg0KYGBge3J9DQojIE1ha2UgYSBjb3B5IG9mIGJsZW5kZWQgc3RhY2sNCmJsZW5kZWRfbGluX3hnX2Z1bGxkYXRhIDwtIGJsZW5kZWRfbGluX3hnDQoNCiMgU3Vic3RpdHV0ZSB0aGUgdHJhaW4gY29tcG9uZW50IHdpdGggdGhlIHdob2xlIGluaXRpYWwgZGF0YQ0KYmxlbmRlZF9saW5feGdfZnVsbGRhdGEkdHJhaW4gPC0gcGxheV9kYXRhDQoNCiMgVHJhaW4gdGhlIHN0YWNrcyBvYmplY3Qgb24gdGhlIHdob2xlIGRhdGENCmJsZW5kZWRfbGluX3hnX2Z1bGxkYXRhX2ZpdCA8LSBibGVuZGVkX2xpbl94Z19mdWxsZGF0YSAlPiUgDQogIGZpdF9tZW1iZXJzKCkNCg0KIyBGdW5jdGlvbiB0byBtYWtlIGF1Z21lbnQoKSB3b3JrIHdpdGggc3RhY2tzDQphdWdtZW50Lm1vZGVsX3N0YWNrIDwtIGZ1bmN0aW9uKHN0YWNrX29iamVjdCwgZGF0YSl7DQogIGRhdGEgJT4lIA0KICAgIGJpbmRfY29scyhwcmVkaWN0KHN0YWNrX29iamVjdCwgLikpDQp9DQoNCiMgUHJlZGljdCBvbiBob2xkb3V0DQpibGVuZGVkX2xpbl94Z19mdWxsZGF0YV9maXQgJT4lIA0KICBwcmVkaWN0X2hvbGRvdXQoKSAlPiUgDQogIHdyaXRlX2NzdigibGluX3hnX3N0YWNrX3ByZWQuY3N2IikNCg0KYGBgDQoNCiMjIEZ1cnRoZXIgcmVhZGluZw0KDQo8aHR0cHM6Ly93d3cua2hhbmFjYWRlbXkub3JnL21hdGgvYWxnZWJyYS1ob21lL2FsZy1leHAtYW5kLWxvZy9hbGctbG9nYXJpdGhtaWMtc2NhbGUvdi9sb2dhcml0aG1pYy1zY2FsZT4NCg0KPGh0dHBzOi8vc3RhdHMuc3RhY2tleGNoYW5nZS5jb20vcXVlc3Rpb25zLzI5OC9pbi1saW5lYXItcmVncmVzc2lvbi13aGVuLWlzLWl0LWFwcHJvcHJpYXRlLXRvLXVzZS10aGUtbG9nLW9mLWFuLWluZGVwZW5kZW50LXZhPg0KDQpJbnZlcnNlIERvY3VtZW50IEZyZXF1ZW5jeTogPGh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90ZmlkZi5odG1sPg0KDQpHZXR0aW5nIHN0YXJ0ZWQgd2l0aCBzdGFja3M6IDxodHRwczovL3N0YWNrcy50aWR5bW9kZWxzLm9yZy9hcnRpY2xlcy9iYXNpY3MuaHRtbD4NCg==