Data understanding

In Data Understanding, you:

  • Import data
  • Clean data
  • Format data properly
  • Create new variables
  • Get an overview about the complete data
  • Split data into training and test set using stratified sampling
  • Discover and visualize the training data to gain insights

Setup

If you like to install all packages at once, use the code below.

#install.packages(c("tidyverse", "skimr", "GGally", "ggmap", "visdat", "corrr", "ggsignif", "gt", "vip", "themis", "purrr", "tidyr", "tidymodels", "keras", "ranger", "xgboost", "kknn")) 

library(tidyverse)   # Collection of R packages for data manipulation, visualization, and analysis  
── Attaching core tidyverse packages ─────────────────────────────────────────────────────────────────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.0.4     ── Conflicts ───────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the ]8;;http://conflicted.r-lib.org/conflicted package]8;; to force all conflicts to become errors
library(skimr)       # Provides enhanced summary statistics for data frames  
Registered S3 methods overwritten by 'htmltools':
  method               from         
  print.html           tools:rstudio
  print.shiny.tag      tools:rstudio
  print.shiny.tag.list tools:rstudio
library(GGally)      # Extension of ggplot2 for correlation matrices, scatterplot matrices, and more  
Registered S3 method overwritten by 'GGally':
  method from   
  +.gg   ggplot2
library(ggmap)       # Enables working with spatial data and maps using ggplot2  
ℹ Google's Terms of Service: ]8;;https://mapsplatform.google.com<https://mapsplatform.google.com>]8;;
  Stadia Maps' Terms of Service: ]8;;https://stadiamaps.com/terms-of-service/<https://stadiamaps.com/terms-of-service/>]8;;
  OpenStreetMap's Tile Usage Policy: ]8;;https://operations.osmfoundation.org/policies/tiles/<https://operations.osmfoundation.org/policies/tiles/>]8;;
ℹ Please cite ggmap if you use it! Use `citation("ggmap")` for details.
library(visdat)      # Visualizes missing values and data structure for exploratory data analysis  
library(corrr)       # Focused on correlation analysis and visualization  

Attaching package: ‘corrr’

The following object is masked from ‘package:skimr’:

    focus
library(ggsignif)    # Adds significance indicators (e.g., p-values) to ggplot2 plots  
library(gt)          # Creates visually appealing tables in R  
library(vip)         # Generates variable importance plots for machine learning models  

Attaching package: ‘vip’

The following object is masked from ‘package:utils’:

    vi
library(themis)      # Helps with dealing with class imbalance in machine learning datasets  
Loading required package: recipes

Attaching package: ‘recipes’

The following object is masked from ‘package:stringr’:

    fixed

The following object is masked from ‘package:stats’:

    step
library(purrr)       # Provides functional programming tools for iteration and mapping  
library(tidyr)       # Helps reshape and tidy data for easier analysis  
library(tidymodels)  # Framework for modeling and machine learning using tidy principles  
── Attaching packages ───────────────────────────────────────────────────────────────────────────────────────────────── tidymodels 1.3.0 ──
✔ broom        1.0.7     ✔ rsample      1.2.1
✔ dials        1.4.0     ✔ tune         1.3.0
✔ infer        1.0.7     ✔ workflows    1.2.0
✔ modeldata    1.4.0     ✔ workflowsets 1.1.0
✔ parsnip      1.3.0     ✔ yardstick    1.3.2
── Conflicts ──────────────────────────────────────────────────────────────────────────────────────────────────── tidymodels_conflicts() ──
✖ scales::discard() masks purrr::discard()
✖ dplyr::filter()   masks stats::filter()
✖ recipes::fixed()  masks stringr::fixed()
✖ dplyr::lag()      masks stats::lag()
✖ yardstick::spec() masks readr::spec()
✖ recipes::step()   masks stats::step()
• Use suppressPackageStartupMessages() to eliminate package startup messages
library(keras)       # Interface to TensorFlow for deep learning in R  

Attaching package: ‘keras’

The following object is masked from ‘package:yardstick’:

    get_weights
library(ranger)      # Fast implementation of random forests for classification and regression  
ranger 0.17.0 using 2 threads (default). Change with num.threads in ranger() and predict(), options(Ncpus = N), options(ranger.num.threads = N) or environment variable R_RANGER_NUM_THREADS.
library(xgboost)     # Optimized gradient boosting framework for predictive modeling  

Attaching package: ‘xgboost’

The following object is masked from ‘package:dplyr’:

    slice
library(kknn)        # Implements k-Nearest Neighbors (kNN) classification and regression 
library(discrim)     # For discrim_linear classification model specifications using the MASS engine.

Attaching package: ‘discrim’

The following object is masked from ‘package:dials’:

    smoothness

Import Data

First of all, let’s import the data:

LINK <- "https://raw.githubusercontent.com/kirenz/datasets/master/housing_unclean.csv"
housing_df <- read_csv(LINK)
Rows: 20640 Columns: 10── Column specification ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Delimiter: ","
chr (3): housing_median_age, median_house_value, ocean_proximity
dbl (7): longitude, latitude, total_rooms, total_bedrooms, population, households, median_income
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Clean data

To get a first impression of the data we take a look at the top 4 rows:

housing_df |>
  slice_head(n = 4) |>
  gt() # print output using gt
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value ocean_proximity
-122.23 37.88 41.0years 880 129 322 126 8.3252 452600.0$ NEAR BAY
-122.22 37.86 21.0 7099 1106 2401 1138 8.3014 358500.0 NEAR BAY
-122.24 37.85 52.0 1467 190 496 177 7.2574 352100.0 NEAR BAY
-122.25 37.85 52.0 1274 235 558 219 5.6431 341300.0 NEAR BAY

Notice the values in the first row of the variables housing_median_age and median_house_value. We need to remove the strings “years” and “$”. Therefore, we use the function str_remove_all from the stringr package. Since there could be multiple wrong entries of the same type, we apply our corrections to all of the rows of the corresponding variable:

housing_df <- 
  housing_df |>
  mutate(
    housing_median_age = str_remove_all(housing_median_age, "[years]"),
    median_house_value = str_remove_all(median_house_value, "[$]")
  )

We don’t cover the phase of data cleaning in detail in this tutorial. However, in a real data science project, data cleaning is usually a very time consuming process.

Format data

Next, we take a look at the data structure and check wether all data formats are correct:
- Numeric variables should be formatted as integers (int) or double precision floating point numbers (dbl).
- Categorical (nominal and ordinal) variables should usually be formatted as factors (fct) and not characters (chr). Especially, if they don’t have many levels.

glimpse(housing_df)
Rows: 20,640
Columns: 10
$ longitude          <dbl> -122.23, -122.22, -122.24, -122.25, -122.25, -122.25, -122.25, -122.25, -122.26, -122.25, -122.26, -122.26, -1…
$ latitude           <dbl> 37.88, 37.86, 37.85, 37.85, 37.85, 37.85, 37.84, 37.84, 37.84, 37.84, 37.85, 37.85, 37.85, 37.84, 37.85, 37.85…
$ housing_median_age <chr> "41.0", "21.0", "52.0", "52.0", "52.0", "52.0", "52.0", "52.0", "42.0", "52.0", "52.0", "52.0", "52.0", "52.0"…
$ total_rooms        <dbl> 880, 7099, 1467, 1274, 1627, 919, 2535, 3104, 2555, 3549, 2202, 3503, 2491, 696, 2643, 1120, 1966, 1228, 2239,…
$ total_bedrooms     <dbl> 129, 1106, 190, 235, 280, 213, 489, 687, 665, 707, 434, 752, 474, 191, 626, 283, 347, 293, 455, 298, 184, 367,…
$ population         <dbl> 322, 2401, 496, 558, 565, 413, 1094, 1157, 1206, 1551, 910, 1504, 1098, 345, 1212, 697, 793, 648, 990, 690, 40…
$ households         <dbl> 126, 1138, 177, 219, 259, 193, 514, 647, 595, 714, 402, 734, 468, 174, 620, 264, 331, 303, 419, 275, 166, 366,…
$ median_income      <dbl> 8.3252, 8.3014, 7.2574, 5.6431, 3.8462, 4.0368, 3.6591, 3.1200, 2.0804, 3.6912, 3.2031, 3.2705, 3.0750, 2.6736…
$ median_house_value <chr> "452600.0", "358500.0", "352100.0", "341300.0", "342200.0", "269700.0", "299200.0", "241400.0", "226700.0", "2…
$ ocean_proximity    <chr> "NEAR BAY", "NEAR BAY", "NEAR BAY", "NEAR BAY", "NEAR BAY", "NEAR BAY", "NEAR BAY", "NEAR BAY", "NEAR BAY", "N…

The package visdat helps us to explore the data class structure visually:

vis_dat(housing_df)

We can observe that the numeric variables housing_media_age and median_house_value are declared as characters (chr) instead of numeric. We choose to format the variables as dbl, since the values could be floating-point numbers.

Furthermore, the categorical variable ocean_proximity is formatted as character instead of factor. Let’s take a look at the levels of the variable:

housing_df |>
  count(ocean_proximity,
        sort = TRUE)

The variable has only 5 levels and therefore should be formatted as a factor.

Note that it is usually a good idea to first take care of the numerical variables. Afterwards, we can easily convert all remaining character variables to factors using the function across from the dplyr package (which is part of the tidyverse).

# convert to numeric
housing_df <- 
  housing_df |>
  mutate(
    housing_median_age = as.numeric(housing_median_age),
    median_house_value = as.numeric(median_house_value)
  )

# convert all remaining character variables to factors 
housing_df <- 
  housing_df |>
  mutate(across(where(is.character), as.factor))

Missing data

Now let’s turn our attention to missing data. Missing data can be viewed with the function vis_miss from the package visdat. We arrange the data by columns with most missingness:

vis_miss(housing_df, sort_miss = TRUE)

Here an alternative method to obtain missing data:

is.na(housing_df) |> colSums()
         longitude           latitude housing_median_age        total_rooms     total_bedrooms         population         households 
                 0                  0                  0                  0                207                  0                  0 
     median_income median_house_value    ocean_proximity 
                 0                  0                  0 

We have a missing rate of 0.1% (207 cases) in our variable total_bedroms. This can cause problems for some algorithms. We will take care of this issue during our data preparation phase.

Create new variables

One very important thing you may want to do at the beginning of your data science project is to create new variable combinations. For example:
- the total number of rooms in a district is not very useful if you don’t know how many households there are. What you really want is the number of rooms per household.
- Similarly, the total number of bedrooms by itself is not very useful: you probably want to compare it to the number of rooms.
- And the population per household also seems like an interesting attribute combination to look at.

Let’s create these new attributes:

housing_df <- 
  housing_df |>
  mutate(rooms_per_household = total_rooms/households,
        bedrooms_per_room = total_bedrooms/total_rooms,
        population_per_household = population/households)

Furthermore, in our example we need to create our dependent variable and drop the original numeric variable.

housing_df <- 
  housing_df |>
  mutate(price_category = case_when( 
    median_house_value < 150000 ~ "below",
    median_house_value >= 150000 ~ "above"
    )) |>
  mutate(price_category = as.factor(price_category)) |>
  dplyr::select(-median_house_value) # avoid conflicts with other packages like MASS

Since we created the new label price_category from the variable median_house_value it is crucial that we never use the variable median_house_value as a predictor in our models. Therefore we drop it.

Take a look at our dependent variable and create a table with the package gt

housing_df |>
  count(price_category, # count observations
        name ="districts_total") |> # name the new variable 
  mutate(percent = districts_total/sum(districts_total)) |> # calculate percentages
  gt() # create table
price_category districts_total percent
above 13084 0.6339147
below 7556 0.3660853

Let’s make a nice looking table:

housing_df |>
  count(price_category, 
        name ="districts_total") %>%
  mutate(percent = districts_total/sum(districts_total)*100,
         percent = round(percent, 2)) %>%
 gt() %>%
  tab_header(
    title = "California median house prices",
    subtitle = "Districts above and below 150.000$"
  ) %>%
  cols_label(
    price_category = "Price",
    districts_total = "Districts",
    percent = "Percent"
  ) |>
  fmt_number(
    columns = vars(districts_total),
    suffixing = TRUE
  ) 
Warning: Since gt v0.3.0, `columns = vars(...)` has been deprecated.
• Please use `columns = c(...)` instead.
California median house prices
Districts above and below 150.000$
Price Districts Percent
above 13.08K 63.39
below 7.56K 36.61

Data overview

After we took care of our data issues, we can obtain a data summary of all numerical and categorical attributes using a function from the package skimr:

skim(housing_df)
── Data Summary ────────────────────────
                           Values    
Name                       housing_df
Number of rows             20640     
Number of columns          13        
_______________________              
Column type frequency:               
  factor                   2         
  numeric                  11        
________________________             
Group variables            None      

We have 20640 observations and 13 columns in our data.

  • The sd column shows the standard deviation, which measures how dispersed the values are.

  • The p0, p25, p50, p75 and p100 columns show the corresponding percentiles: a percentile indicates the value below which a given percentage of observations in a group of observations fall. For example, 25% of the districts have a housing_median_age lower than 18, while 50% are lower than 29 and 75% are lower than 37. These are often called the 25th percentile (or first quartile), the median, and the 75th percentile.

  • Further note that the median income attribute does not look like it is expressed in US dollars (USD). Actually the data has been scaled and capped at 15 (actually, 15.0001) for higher median incomes, and at 0.5 (actually, 0.4999) for lower median incomes. The numbers represent roughly tens of thousands of dollars (e.g., 3 actually means about $30,000).

Another quick way to get an overview of the type of data you are dealing with is to plot a histogram for each numerical attribute. A histogram shows the number of instances (on the vertical axis) that have a given value range (on the horizontal axis). You can either plot this one attribute at a time, or you can use ggscatmat from the package GGally on the whole dataset (as shown in the following code example), and it will plot a histogram for each numerical attribute as well as correlation coefficients (Pearson is the default). We just select the most promising variabels for our plot:

housing_df |>
  dplyr::select(
    housing_median_age, 
    median_income, bedrooms_per_room, rooms_per_household, 
    population_per_household) |>
  ggscatmat(alpha = 0.2)

Another option is to use ggpairs, where we even can integrate categorical variables like our dependent variable price_category and ocean proximity in the output:

housing_df |>
  dplyr::select(
    housing_median_age, 
    median_income, bedrooms_per_room, rooms_per_household, 
    population_per_household, ocean_proximity,
    price_category) |>
  ggpairs()

There are a few things you might notice in these histograms:

  • The variables median income, housing median age were capped.

  • Note that our attributes have very different scales. We will take care of this issue later in data preparation, when we use feature scaling (data normalization).

  • Finally, many histograms are tail-heavy: they extend much farther to the right of the median than to the left. This may make it a bit harder for some Machine Learning algorithms to detect patterns. We will transform these attributes later on to have more bell-shaped distributions. For our right-skewed data (i.e., tail is on the right, also called positive skew), common transformations include square root and log (we will use the log).

Data splitting

Before we get started with our in-depth data exploration, let’s split our single dataset into two: a training set and a testing set. The training data will be used to fit models, and the testing set will be used to measure model performance. We perform data exploration only on the training data.

A training dataset is a dataset of examples used during the learning process and is used to fit the models. A test dataset is a dataset that is independent of the training dataset and is used to evaluate the performance of the final model. If a model fit to the training dataset also fits the test dataset well, minimal overfitting has taken place. A better fitting of the training dataset as opposed to the test dataset usually points to overfitting.

In our data split, we want to ensure that the training and test set is representative of the categories of our dependent variable.

housing_df |>
  ggplot(aes(price_category)) +
  geom_bar() 

In general, we would like to have instances for each stratum, or else the estimate of a stratum’s importance may be biased. A stratum (plural strata) refers to a subset (part) of the whole data from which is being sampled. We only have two categories in our data.

To actually split the data, we can use the rsample package (included in tidymodels) to create an object that contains the information on how to split the data (which we call data_split), and then two more rsample functions to create data frames for the training and testing sets:

# Fix the random numbers by setting the seed 
# This enables the analysis to be reproducible 
set.seed(123)

# Put 3/4 of the data into the training set 
data_split <- initial_split(housing_df, 
                           prop = 3/4, 
                           strata = price_category)

# Create dataframes for the two sets:
train_data <- training(data_split) 
test_data <- testing(data_split)

Data exploration

The point of data exploration is to gain insights that will help you select important variables for your model and to get ideas for feature engineering in the data preparation phase. Ususally, data exploration is an iterative process: once you get a prototype model up and running, you can analyze its output to gain more insights and come back to this exploration step. It is important to note that we perform data exploration only with our training data.

Create data copy

We first make a copy of the training data since we don’t want to alter our data during data exploration.

data_explore <- train_data

Next, we take a closer look at the relationships between our variables. In particular, we are interested in the relationships between ur dependent variable price_category and all other variables. The goal is to identify possible predictor variables which we could use in our models to predict the price_category.

Geographical overview

Since our data includes information about longitude and latitude, we start our data exploration with the creation of a geographical scatterplot of the data to get some first insights:

data_explore |>
  ggplot(aes(x = longitude, y = latitude)) +
  geom_point(color = "cornflowerblue")

A better visualization that highlights high-density areas (with parameter alpha = 0.1 ):

data_explore |>
  ggplot(aes(x = longitude, y = latitude)) +
  geom_point(color = "cornflowerblue", alpha = 0.1) 

Overview about California housing prices:
- red is expensive,
- teal is cheap and
- larger circles indicate areas with a larger population.

data_explore |>
  ggplot(aes(x = longitude, y = latitude)) +
  geom_point(aes(size = population, color = price_category), 
             alpha = 0.4)

Numerical variables

We can use boxplots to check, if we actually find differences in our numeric variables for the different levels of our dependent categorical variable:

data_explore |>
  ggplot(aes(x = price_category, y = median_income, 
             fill = price_category, color = price_category)) +
  geom_boxplot(alpha=0.4) 

Let`s define a function for this task that accepts strings as inputs so we don’t have to copy and paste our code for every plot. Note that we only have to change the “y-variable” in every plot.

print_boxplot <- function(.y_var){
  
  # convert strings to variable
  y_var <- sym(.y_var) 
 
  # unquote variables using {{}}
  data_explore |>
  ggplot(aes(x = price_category, y = {{y_var}},
             fill = price_category, color = price_category)) +
  geom_boxplot(alpha=0.4) 
  
}  

Obtain all of the names of the y-variables we want to use for our plots:

y_var <- 
  data_explore |>
  dplyr::select(where(is.numeric), -longitude, - latitude) |>
  variable.names() # obtain name

The map function applys the function print_boxplot to each element of our atomic vector y_var and returns the according plot:

map(y_var, print_boxplot)
[[1]]

[[2]]

[[3]]

[[4]]

[[5]]

[[6]]

[[7]]

[[8]]

[[9]]

We can observe a difference in the price_category:

  • The differences between our two groups are quite small for housing_median_age, total_room, total_bedrooms, population and households

  • We can observe a noticeable difference for our variables median_income and bedrooms_per_room

  • population_per_household and rooms_per_household include some extreme values. We first need to fix this before we can proceed with our interpretations for this variables.

Again, let’s write a short function for this task and filter some of the extreme cases. We call the new function print_boxplot_out:

print_boxplot_out <- function(.y_var_out){
  
  y_var <- sym(.y_var_out) 
 
  data_explore |>
  filter(rooms_per_household < 50, population_per_household < 20) |>
  ggplot(aes(x = price_category, y = {{y_var}},
             fill = price_category, color = price_category)) +
  geom_boxplot(alpha=0.4) 
  
} 

y_var_out <- 
  data_explore |>
  dplyr::select(rooms_per_household, population_per_household) |>
  variable.names() 

map(y_var_out, print_boxplot_out)
[[1]]

[[2]]

Now we are able to recognize a small difference for population_per_household. rooms_per_household on the other hand is quite similar for both groups.

Additionally, we can use the function ggscatmat to create plots with our dependent variable as color column:

data_explore |>
  dplyr::select(price_category, median_income, bedrooms_per_room, rooms_per_household, 
         population_per_household) |>
  ggscatmat(color="price_category", 
            corMethod = "spearman",
            alpha=0.2)
Warning: Factor variables are omitted in plot

There are a few things you might notice in these histograms:

  • Note that our attributes have very different scales. We will take care of this issue later in data preparation, when we use feature scaling (data normalization).

  • The histograms are tail-heavy: they extend much farther to the right of the median than to the left. This may make it a bit harder for some Machine Learning algorithms to detect patterns. We will transform these attributes later on to have more bell-shaped distributions. For our right-skewed data (i.e., tail is on the right, also called positive skew), common transformations include square root and log (we will use the log).

As a result of our data exploration, we will include the numerical variables

  • median_income,
  • bedrooms_per_room and
  • population_per_household

as predictors in our model.

Categorical variables

Now let’s analyze the relationship between our categorical variables ocean proximity and price_category. We start with a simple count.

data_explore |>
  count(price_category, ocean_proximity) |>
  group_by(price_category) |>
  mutate(percent = n / sum(n) *100,
         percent = round(percent, 2)) |>
  gt() |>
    tab_header(
    title = "California median house prices",
    subtitle = "Districts above and below 150.000$"
  ) |>
  cols_label(
    ocean_proximity = "Ocean Proximity",
    n = "Districts",
    percent = "Percent"
  ) |>
  fmt_number(
    columns = vars(n),
    suffixing = TRUE
  ) 
Warning: Since gt v0.3.0, `columns = vars(...)` has been deprecated.
• Please use `columns = c(...)` instead.
California median house prices
Districts above and below 150.000$
Ocean Proximity Districts Percent
above
<1H OCEAN 5.69K 58.00
INLAND 1.24K 12.62
ISLAND 3.00 0.03
NEAR BAY 1.37K 13.99
NEAR OCEAN 1.51K 15.36
below
<1H OCEAN 1.13K 19.98
INLAND 3.71K 65.40
NEAR BAY 345.00 6.09
NEAR OCEAN 484.00 8.54

The function geom_bin2d() creats a heatmap by counting the number of cases in each group, and then mapping the number of cases to each subgroub’s fill.

data_explore %>%
  ggplot(aes(price_category, ocean_proximity)) +
  geom_bin2d() +
  scale_fill_continuous(type = "viridis") 

We can observe that most districts with a median house price above 150,000 have an ocean proximity below 1 hour. On the other hand, districts below that threshold are typically inland. Hence, ocean proximity is indeed a good predictor for our two different median house value categories.

Data preparation

  • Handle missing values
  • Fix or remove outliers
  • Feature selection
  • Feature engineering
  • Feature scaling
  • Create a validation set

Next, we’ll preprocess our data before training the models. We mainly use the tidymodels packages recipes and workflows for these steps. Recipes are built as a series of optional data preparation steps, such as:

  • Data cleaning: Fix or remove outliers, fill in missing values (e.g., with zero, mean, median…) or drop their rows (or columns).

  • Feature selection: Drop the attributes that provide no useful information for the task.

  • Feature engineering: Discretize continuous features, decompose features (e.g., the weekday from a date variable, etc.), add promising transformations of features (e.g., log(x), sqrt(x), x2 , etc.) or aggregate features into promising new features (like we already did).

  • Feature scaling: Standardize or normalize features.

We will want to use our recipe across several steps as we train and test our models. To simplify this process, we can use a model workflow, which pairs a model and recipe together.

Data preparation

Before we create our recipes, we first select the variables which we will use in the model. Note that we keep longitude and latitude to be able to map the data in a later stage but we will not use the variables in our model.

housing_df_new <-
  housing_df |>
  dplyr::select( # select our predictors
    longitude, latitude, 
    price_category, 
    median_income, 
    ocean_proximity, 
    bedrooms_per_room, 
    rooms_per_household, 
    population_per_household
         )

glimpse(housing_df_new)
Rows: 20,640
Columns: 8
$ longitude                <dbl> -122.23, -122.22, -122.24, -122.25, -122.25, -122.25, -122.25, -122.25, -122.26, -122.25, -122.26, -122.…
$ latitude                 <dbl> 37.88, 37.86, 37.85, 37.85, 37.85, 37.85, 37.84, 37.84, 37.84, 37.84, 37.85, 37.85, 37.85, 37.84, 37.85,…
$ price_category           <fct> above, above, above, above, above, above, above, above, above, above, above, above, above, above, above,…
$ median_income            <dbl> 8.3252, 8.3014, 7.2574, 5.6431, 3.8462, 4.0368, 3.6591, 3.1200, 2.0804, 3.6912, 3.2031, 3.2705, 3.0750, …
$ ocean_proximity          <fct> NEAR BAY, NEAR BAY, NEAR BAY, NEAR BAY, NEAR BAY, NEAR BAY, NEAR BAY, NEAR BAY, NEAR BAY, NEAR BAY, NEAR…
$ bedrooms_per_room        <dbl> 0.1465909, 0.1557966, 0.1295160, 0.1844584, 0.1720959, 0.2317737, 0.1928994, 0.2213273, 0.2602740, 0.199…
$ rooms_per_household      <dbl> 6.984127, 6.238137, 8.288136, 5.817352, 6.281853, 4.761658, 4.931907, 4.797527, 4.294118, 4.970588, 5.47…
$ population_per_household <dbl> 2.555556, 2.109842, 2.802260, 2.547945, 2.181467, 2.139896, 2.128405, 1.788253, 2.026891, 2.172269, 2.26…

Furthermore, we need to make a new data split since we updated the original data.

set.seed(123)

data_split <- initial_split(housing_df_new, # updated data
                           prop = 3/4, 
                           strata = price_category)

train_data <- training(data_split) 
test_data <- testing(data_split)

Data prepropecessing recipe

The type of data preprocessing is dependent on the data and the type of model being fit. The excellent book “Tidy Modeling with R” provides an appendix with recommendations for baseline levels of preprocessing that are needed for various model functions.

Let’s create a base recipe for all of our classification models. Note that the sequence of steps matter:

  • The recipe() function has two arguments:

    1. A formula. Any variable on the left-hand side of the tilde (~) is considered the model outcome (here, price_category). On the right-hand side of the tilde are the predictors. Variables may be listed by name (separated by a +), or you can use the dot (.) to indicate all other variables as predictors.

    2. The data. A recipe is associated with the data set used to create the model. This will typically be the training set, so data = train_data here.

  • update_role(): This step of adding roles to a recipe is optional; the purpose of using it here is that those two variables can be retained in the data but not included in the model. This can be convenient when, after the model is fit, we want to investigate some poorly predicted value. These ID columns will be available and can be used to try to understand what went wrong.

  • step_naomit() removes observations (rows of data) if they contain NA or NaN values. We use skip = TRUE because we don’t want to perform this part to new data so that the number of samples in the assessment set is the same as the number of predicted values (even if they are NA).

Note that instead of deleting missing values we could also easily substitute (i.e., impute) missing values of variables by one of the following methods (using the training set):

Take a look at the recipes reference for an overview about all possible imputation methods.

  • step_novel() converts all nominal variables to factors and takes care of other issues related to categorical variables.

  • step_log() will log transform data (since some of our numerical variables are right-skewed). Note that this step can not be performed on negative numbers.

  • step_normalize() normalizes (center and scales) the numeric variables to have a standard deviation of one and a mean of zero. (i.e., z-standardization).

  • step_dummy() converts our factor column ocean_proximity into numeric binary (0 and 1) variables.

Note that this step may cause problems if your categorical variable has too many levels - especially if some of the levels are very infrequent. In this case you should either drop the variable or pool infrequently occurring values into an “other” category with [step_other](https://recipes.tidymodels.org/reference/step_other.html). This steps has to be performed before step_dummy.

  • step_zv(): removes any numeric variables that have zero variance.

  • step_corr(): will remove predictor variables that have large correlations with other predictor variables.

Note that the package themis contains extra steps for the recipes package for dealing with imbalanced data. A classification data set with skewed class proportions is called imbalanced. Classes that make up a large proportion of the data set are called majority classes. Those that make up a smaller proportion are minority classes (see Google Developers for more details). Themis provides various methods for over-sampling (e.g. SMOTE) and under-sampling. However, we don’t have to use this methods since our data is not imbalanced.

housing_rec <-
  recipe(price_category ~ .,
         data = train_data) %>%
  update_role(longitude, latitude, new_role = "ID") %>%
  step_log(median_income,
           bedrooms_per_room, rooms_per_household, 
           population_per_household) %>%
  step_naomit(everything(), skip = TRUE) %>%
  step_novel(all_nominal(), -all_outcomes()) %>%
  # Make sure all nominal predictors (including ocean_proximity) are dummied
  step_dummy(all_nominal_predictors(), -all_outcomes()) %>%
  step_normalize(all_numeric(), -all_outcomes(), -longitude, -latitude) %>%
  step_zv(all_numeric(), -all_outcomes()) %>%
  step_corr(all_predictors(), threshold = 0.7, method = "spearman") %>%
  step_smote(price_category) 

To view the current set of variables and roles, use the summary() function:

summary(housing_rec)

If we would like to check if all of our preprocessing steps from above actually worked, we can proceed as follows:

prepped_data <- 
  housing_rec |># use the recipe object
  prep() |># perform the recipe on training data
  juice() # extract only the preprocessed dataframe 
Warning: !  The following column has zero variance so scaling cannot be used: ocean_proximity_new.
ℹ Consider using ]8;;ide:help:recipes::step_zv?step_zv]8;; to remove those columns before normalizing.

Take a look at the data structure:

glimpse(prepped_data)
Rows: 19,414
Columns: 10
$ longitude                  <dbl> -122.23, -122.22, -122.24, -122.25, -122.25, -122.25, -122.26, -122.25, -122.26, -122.26, -122.26, -12…
$ latitude                   <dbl> 37.88, 37.86, 37.85, 37.85, 37.85, 37.84, 37.84, 37.84, 37.85, 37.85, 37.85, 37.84, 37.85, 37.85, 37.8…
$ median_income              <dbl> 1.842052e+00, 1.836006e+00, 1.552164e+00, 2.112520e-01, 3.133968e-01, 1.059356e-01, -1.086555e+00, 1.2…
$ rooms_per_household        <dbl> 1.06765323, 0.65680009, 1.69028872, 0.68220035, -0.32555744, -0.19778338, -0.70146222, -0.16936755, 0.…
$ population_per_household   <dbl> -0.3913634897, -1.0996745281, -0.0507778478, -0.9762937735, -1.0474005776, -1.0673009190, -1.247910248…
$ price_category             <fct> above, above, above, above, above, above, above, above, above, above, above, above, above, above, abov…
$ ocean_proximity_INLAND     <dbl> -0.6861777, -0.6861777, -0.6861777, -0.6861777, -0.6861777, -0.6861777, -0.6861777, -0.6861777, -0.686…
$ ocean_proximity_ISLAND     <dbl> -0.01399229, -0.01399229, -0.01399229, -0.01399229, -0.01399229, -0.01399229, -0.01399229, -0.01399229…
$ ocean_proximity_NEAR.BAY   <dbl> 2.829997, 2.829997, 2.829997, 2.829997, 2.829997, 2.829997, 2.829997, 2.829997, 2.829997, 2.829997, 2.…
$ ocean_proximity_NEAR.OCEAN <dbl> -0.3834987, -0.3834987, -0.3834987, -0.3834987, -0.3834987, -0.3834987, -0.3834987, -0.3834987, -0.383…

Visualize the numerical data:

prepped_data |>
  dplyr::select(price_category, 
         median_income, 
         rooms_per_household, 
         population_per_household) |>
  ggscatmat(corMethod = "spearman",
            alpha=0.2)
Warning: Factor variables are omitted in plot

You should notice that:

  • the variables longitude and latitude did not change.

  • median_income, rooms_per_household and population_per_household are now z-standardized and the distributions are a bit less right skewed (due to our log transformation)

  • ocean_proximity was replaced by dummy variables.

Validation set

Remember that we already partitioned our data set into a training set and test set. This lets us judge whether a given model will generalize well to new data. However, using only two partitions may be insufficient when doing many rounds of hyperparameter tuning (which we don’t perform in this tutorial but it is always recommended to use a validation set).

Therefore, it is usually a good idea to create a so called validation set. Watch this short video from Google’s Machine Learning crash course to learn more about the value of a validation set.

We use k-fold cross validation to build a set of 5 validation folds with the function vfold_cv. We also use stratified sampling:

set.seed(100)

cv_folds <-
 vfold_cv(train_data, 
          v = 5, 
          strata = price_category) 

We will come back to the validation set after we specified our models.

Model building

Specify models

The process of specifying our models is always as follows:

  1. Pick a model type
  2. Set the engine
  3. Set the mode: regression or classification

You can choose the model type and engine from this list.

Logistic regression

log_spec <- # your model specification
  logistic_reg() |> # model type
  set_engine(engine = "glm") |> # model engine
  set_mode("classification") # model mode

# Show your model specification
log_spec
Logistic Regression Model Specification (classification)

Computational engine: glm 

Linear discriminant

lda_spec <-
  discrim_linear() |>
  set_engine(engine='MASS') |>
  set_mode("classification")

# Show your model specification
lda_spec
Linear Discriminant Model Specification (classification)

Computational engine: MASS 

Quadratic discriminant

qda_spec <-
  discrim_quad() |>
  set_engine(engine='MASS') |>
  set_mode("classification")

# Show your model specification
qda_spec
Quadratic Discriminant Model Specification (classification)

Computational engine: MASS 

K-nearest neighbor

knn_spec <- 
  nearest_neighbor(neighbors = 4) |># we can adjust the number of neighbors 
  set_engine("kknn") |>
  set_mode("classification") 

Create workflows

To combine the data preparation recipe with the model building, we use the package workflows. A workflow is an object that can bundle together your pre-processing recipe, modeling, and even post-processing requests (like calculating the RMSE).

Logistic regression

Bundle recipe and model with workflows:

log_wflow <- # new workflow object
 workflow() |># use workflow function
 add_recipe(housing_rec) |>  # use the new recipe
 add_model(log_spec)   # add your model spec

# show object
log_wflow
══ Workflow ═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: logistic_reg()

── Preprocessor ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
8 Recipe Steps

• step_log()
• step_naomit()
• step_novel()
• step_dummy()
• step_normalize()
• step_zv()
• step_corr()
• step_smote()

── Model ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Logistic Regression Model Specification (classification)

Computational engine: glm 

LDA

Bundle recipe and model:

lda_wflow <- # new workflow object
 workflow() |># use workflow function
 add_recipe(housing_rec) |>  # use the new recipe
 add_model(lda_spec)   # add your model spec

QDA

Bundle recipe and model:

qda_wflow <- # new workflow object
 workflow() |># use workflow function
 add_recipe(housing_rec) |>  # use the new recipe
 add_model(qda_spec)   # add your model spec

K-nearest neighbor

Bundle recipe and model:

knn_wflow <-
 workflow() %>%
 add_recipe(housing_rec) |>
 add_model(knn_spec)

Evaluate models

Now we can use our validation set (cv_folds) to estimate the performance of our models using the fit_resamples() function to fit the models on each of the folds and store the results.

Note that fit_resamples() will fit our model to each resample and evaluate on the holdout set from each resample. The function is usually only used for computing performance metrics across some set of resamples to evaluate our models (like accuracy) - the models are not even stored. However, in our example we save the predictions in order to visualize the model fit and residuals with control_resamples(save_pred = TRUE).

Finally, we collect the performance metrics with collect_metrics() and pick the model that does best on the validation set.

Logistic regression

We use our workflow object to perform resampling. Furthermore, we use metric_set() to choose some common classification performance metrics provided by the yardstick package. Visit yardsticks reference to see the complete list of all possible metrics.

Note that Cohen’s kappa coefficient (\(\kappa\)) is a similar measure to accuracy, but is normalized by the accuracy that would be expected by chance alone and is very useful when one or more classes have large frequency distributions. The higher the value, the better.

# Ensure that all metric functions are from yardstick:
log_res <- 
  log_wflow |>
  fit_resamples(
    resamples = cv_folds, 
    metrics = metric_set(
      yardstick::recall, 
      yardstick::precision, 
      yardstick::f_meas, 
      yardstick::accuracy, 
      yardstick::kap, 
      yardstick::roc_auc, 
      yardstick::sens, 
      yardstick::spec
    ),
    control = control_resamples(save_pred = TRUE)
  )
→ A | warning: !  The following column has zero variance so scaling cannot be used: ocean_proximity_new.
               ℹ Consider using ]8;;ide:help:recipes::step_zv?step_zv]8;; to remove those columns before normalizing.

There were issues with some computations   A: x1

There were issues with some computations   A: x2

There were issues with some computations   A: x3

There were issues with some computations   A: x4

There were issues with some computations   A: x5

There were issues with some computations   A: x5
Model coefficients

The above described method to obtain log_res is fine if we are not interested in model coefficients. However, if we would like to extract the model coefficients from fit_resamples, we need to proceed as follows:

# save model coefficients for a fitted model object from a workflow

get_model <- function(x) {
  extract_fit_parsnip(x) |>tidy()
}

# same as before with one exception
log_res_2 <- 
  log_wflow |>
  fit_resamples(
    resamples = cv_folds, 
    metrics = metric_set(
      yardstick::recall, 
      yardstick::precision, 
      yardstick::f_meas, 
      yardstick::accuracy, 
      yardstick::kap, 
      yardstick::roc_auc, 
      yardstick::sens, 
      yardstick::spec
    ),
    control = control_resamples(
      save_pred = TRUE,
      extract = get_model) # use extract and our new function
    ) 
→ A | warning: !  The following column has zero variance so scaling cannot be used: ocean_proximity_new.
               ℹ Consider using ]8;;ide:help:recipes::step_zv?step_zv]8;; to remove those columns before normalizing.

There were issues with some computations   A: x1

There were issues with some computations   A: x2

There were issues with some computations   A: x3

There were issues with some computations   A: x4

There were issues with some computations   A: x5

There were issues with some computations   A: x5

Now there is a .extracts column with nested tibbles.

log_res_2$.extracts[[1]]

To get the results use:

log_res_2$.extracts[[1]][[1]]
[[1]]
NA

All of the results can be flattened and collected using:

all_coef <- map_dfr(log_res_2$.extracts, ~ .x[[1]][[1]])

Show all of the resample coefficients for a single predictor:

filter(all_coef, term == "median_income")
Performance metrics

Show average performance over all folds (note that we use log_res):

log_res |> collect_metrics(summarize = TRUE)

Show performance for every single fold:

log_res |> collect_metrics(summarize = FALSE)
Collect predictions

To obtain the actual model predictions, we use the function collect_predictions and save the result as log_pred:

log_pred <- 
  log_res %>%
  collect_predictions()
Confusion matrix

Now we can use the predictions to create a confusion matrix with conf_mat():

log_pred |>
  conf_mat(price_category, .pred_class) 
          Truth
Prediction above below
     above  8245   902
     below  1568  4765

Additionally, the confusion matrix can quickly be visualized in different formats using autoplot(). Type mosaic:

log_pred |>
  conf_mat(price_category, .pred_class) |>
  autoplot(type = "mosaic")

Or type heatmap:

log_pred |>
  conf_mat(price_category, .pred_class) |>
  autoplot(type = "heatmap")

ROC-Curve

We can also make an ROC curve for our 5 folds. Since the category we are predicting is the first level in the price_category factor (“above”), we provide roc_curve() with the relevant class probability .pred_above:

log_pred |>
  group_by(id) |># id contains our folds
  roc_curve(price_category, .pred_above) |>
  autoplot()

Visit Google developer’s Machine Learning Crashcourse to learn more about the ROC-Curve.

Probability distributions

Plot predicted probability distributions for our two classes.

log_pred |>
  ggplot() +
  geom_density(aes(x = .pred_above, 
                   fill = price_category), 
               alpha = 0.5)

Random forest

We don’t repeat all of the steps shown in logistic regression and just focus on the performance metrics.

lda_res <-
  lda_wflow |>
  fit_resamples(
    resamples = cv_folds, 
    metrics = metric_set(
      yardstick::recall, 
      yardstick::precision, 
      yardstick::f_meas, 
      yardstick::accuracy, 
      yardstick::kap, 
      yardstick::roc_auc, 
      yardstick::sens, 
      yardstick::spec
    ),
    control = control_resamples(save_pred = TRUE)
    ) 
→ A | warning: !  The following column has zero variance so scaling cannot be used: ocean_proximity_new.
               ℹ Consider using ]8;;ide:help:recipes::step_zv?step_zv]8;; to remove those columns before normalizing.

There were issues with some computations   A: x1

There were issues with some computations   A: x2

There were issues with some computations   A: x3

There were issues with some computations   A: x4

There were issues with some computations   A: x5

There were issues with some computations   A: x5
lda_res |> collect_metrics(summarize = TRUE)

QDA

We don’t repeat all of the steps shown in logistic regression and just focus on the performance metrics.

qda_res <- 
  qda_wflow |>
  fit_resamples(
    resamples = cv_folds, 
    metrics = metric_set(
      yardstick::recall, 
      yardstick::precision, 
      yardstick::f_meas, 
      yardstick::accuracy, 
      yardstick::kap, 
      yardstick::roc_auc, 
      yardstick::sens, 
      yardstick::spec
    ),
    control = control_resamples(save_pred = TRUE)
    ) 

qda_res |>collect_metrics(summarize = TRUE)

#→ A | error:   rank deficiency in group below
#There were issues with some computations   A: x5
#Warning: All models failed. Run `show_notes(.Last.tune.result)` for more information.Error in `estimate_tune_results()`:
#! All models failed. Run `show_notes(.Last.tune.result)` for more information.
#Run `rlang::last_trace()` to see where the error occurred.

The error message suggests that there is a rank deficiency in your qda_res model, meaning that some of the predictor variables are likely collinear (highly correlated) or have near-zero variance, which is problematic for Quadratic Discriminant Analysis (QDA). Here’s how you can troubleshoot and resolve the issue:

1. Check for Near-Zero Variance Predictors
QDA does not handle variables with near-zero variance well. You can check for such predictors using:

library(caret)
Loading required package: lattice

Attaching package: ‘caret’

The following object is masked from ‘package:kknn’:

    contr.dummy

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

    precision, recall, sensitivity, specificity

The following object is masked from ‘package:purrr’:

    lift
nearZeroVar(train_data, saveMetrics = TRUE)

If any variables have near-zero variance, consider removing them. This is not our problem.

2. Check for Collinearity Highly correlated predictors can cause rank deficiency. Compute the correlation matrix:

cor(train_data %>% select_if(is.numeric), use = "pairwise.complete.obs")
                             longitude     latitude median_income bedrooms_per_room rooms_per_household population_per_household
longitude                 1.000000e+00 -0.924523028   -0.01507299       0.089800599        -0.030477506             6.477147e-05
latitude                 -9.245230e-01  1.000000000   -0.08151432      -0.108561739         0.109990761             7.726872e-03
median_income            -1.507299e-02 -0.081514323    1.00000000      -0.615391937         0.347294622             2.331952e-02
bedrooms_per_room         8.980060e-02 -0.108561739   -0.61539194       1.000000000        -0.430096710             6.924601e-03
rooms_per_household      -3.047751e-02  0.109990761    0.34729462      -0.430096710         1.000000000            -9.125535e-03
population_per_household  6.477147e-05  0.007726872    0.02331952       0.006924601        -0.009125535             1.000000e+00

You can remove highly correlated features using something like (Note: I mispelled train_data so this wouldn’t work):

training_data <- training_data %>%
  dplyr::select(-one_of(findCorrelation(cor_matrix, cutoff = 0.9)))

And try rerunning the model after dropping collinear variables. Our largest correlation is between latitude and longitude which makes sense. This is also not our problem.

3. Ensure Balanced Class Distribution
QDA can struggle if classes are highly imbalanced. Check the class distribution:

table(train_data$price_category)

above below 
 9813  5667 

If imbalance exists, try using SMOTE, downsampling, or upsampling which we did above

step_smote(price_category)

4. Check for Missing Values Missing values can also cause errors. Check and handle missing data:

sum(is.na(training_data))
[1] 0

Not this either.

5. Try an Alternative Model Sometimes models can’t fit a data set despite your best efforts. If the problem persists, try another model.

K-nearest neighbor

We don’t repeat all of the steps shown in logistic regression and just focus on the performance metrics.

knn_res <- 
  knn_wflow |>
  fit_resamples(
    resamples = cv_folds, 
    metrics = metric_set(
      yardstick::recall, 
      yardstick::precision, 
      yardstick::f_meas, 
      yardstick::accuracy, 
      yardstick::kap, 
      yardstick::roc_auc, 
      yardstick::sens, 
      yardstick::spec
    ),
    control = control_resamples(save_pred = TRUE)
    ) 
→ A | warning: !  The following column has zero variance so scaling cannot be used: ocean_proximity_new.
               ℹ Consider using ]8;;ide:help:recipes::step_zv?step_zv]8;; to remove those columns before normalizing.

There were issues with some computations   A: x1

There were issues with some computations   A: x2

There were issues with some computations   A: x3

There were issues with some computations   A: x4

There were issues with some computations   A: x5

There were issues with some computations   A: x5
knn_res |> collect_metrics(summarize = TRUE)

Compare models

Extract metrics from our models to compare them:

log_metrics <- 
  log_res |>
  collect_metrics(summarize = TRUE) %>%
  mutate(model = "Logistic Regression") # add the name of the model to every row

lda_metrics <- 
  lda_res |>
  collect_metrics(summarize = TRUE) %>%
  mutate(model = "Linear Discriminant")

# QDA did not run
# qda_metrics <- 
# qda_res |>
#   collect_metrics(summarise = TRUE) %>%
#   mutate(model = "Quadratic Discriminant")

# knn_metrics <- 
#   knn_res |>
#   collect_metrics(summarise = TRUE) %>%
#   mutate(model = "KNN")

# create dataframe with all models
model_compare <- bind_rows(
                          log_metrics,
                          lda_metrics
                           ) 

# change data structure
model_comp <- 
  model_compare |>
  dplyr::select(model, .metric, mean, std_err) |>
  pivot_wider(names_from = .metric, values_from = c(mean, std_err)) 

# show mean F1-Score for every model
model_comp |>
  arrange(mean_f_meas) |>
  mutate(model = fct_reorder(model, mean_f_meas)) |># order results
  ggplot(aes(model, mean_f_meas, fill=model)) +
  geom_col() +
  coord_flip() +
  scale_fill_brewer(palette = "Blues") +
   geom_text(
     size = 3,
     aes(label = round(mean_f_meas, 2), y = mean_f_meas + 0.08),
     vjust = 1
  )

# show mean area under the curve (auc) per model
model_comp |>
  arrange(mean_roc_auc) |>
  mutate(model = fct_reorder(model, mean_roc_auc)) %>%
  ggplot(aes(model, mean_roc_auc, fill=model)) +
  geom_col() +
  coord_flip() +
  scale_fill_brewer(palette = "Blues") + 
     geom_text(
     size = 3,
     aes(label = round(mean_roc_auc, 2), y = mean_roc_auc + 0.08),
     vjust = 1
  )

Note that the model results are all quite similar. In our example we choose the F1-Score as performance measure to select the best model. Let’s find the maximum mean F1-Score:

model_comp |> slice_max(mean_f_meas)

Last evaluation on test set

Tidymodels provides the function last_fit() which fits a model to the whole training data and evaluates it on the test set. We just need to provide the workflow object of the best model as well as the data split object (not the training data).

last_fit_logit <- last_fit(log_wflow, 
                        split = data_split,
                        metrics = metric_set(
                        yardstick::recall, 
                        yardstick::precision, 
                        yardstick::f_meas, 
                        yardstick::accuracy, 
                        yardstick::kap, 
                        yardstick::roc_auc, 
                        yardstick::sens, 
                        yardstick::spec
                        ))
→ A | warning: !  The following column has zero variance so scaling cannot be used: ocean_proximity_new.
               ℹ Consider using ]8;;ide:help:recipes::step_zv?step_zv]8;; to remove those columns before normalizing.

There were issues with some computations   A: x1

There were issues with some computations   A: x1

Show performance metrics

last_fit_logit |>
  collect_metrics()

And these are our final performance metrics. Remember that if a model fit to the training dataset also fits the test dataset well, minimal overfitting has taken place. This seems to be also the case in our example.

To learn more about the model we can access the variable importance scores via the .workflow column. We first need to pluck out the first element in the workflow column, then pull out the fit from the workflow object. Finally, the vip package helps us visualize the variable importance scores for the top features. Note that we can’t create this type of plot for every model engine.

last_fit_logit |>
  pluck(".workflow", 1) |>  
  extract_fit_parsnip() |>
  vip(num_features = 10)

The two most important predictors in whether a district has a median house value above or below $150,000 dollars were the ocean proximity inland and the median income.

Take a look at the confusion matrix:

last_fit_logit %>%
  collect_predictions() |>
  conf_mat(price_category, .pred_class) |>
  autoplot(type = "heatmap")

Let’s create the ROC curve. Again, since the event we are predicting is the first level in the price_category factor (“above”), we provide roc_curve() with the relevant class probability .pred_above:

last_fit_logit |>
  collect_predictions() |>
  roc_curve(price_category, .pred_above) |>
  autoplot()

Based on all of the results, the validation set and test set performance statistics are very close, so we would have pretty high confidence that our logit model would perform well when predicting new data.

LS0tDQp0aXRsZTogIkNsYXNzaWZpY2F0aW9uIGluIFIiDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19mbG9hdDogdHJ1ZQ0KLS0tDQoNCiMjIERhdGEgdW5kZXJzdGFuZGluZw0KSW4gRGF0YSBVbmRlcnN0YW5kaW5nLCB5b3U6ICANCg0KIC0gSW1wb3J0IGRhdGENCiAtIENsZWFuIGRhdGENCiAtIEZvcm1hdCBkYXRhIHByb3Blcmx5DQogLSBDcmVhdGUgbmV3IHZhcmlhYmxlcw0KIC0gR2V0IGFuIG92ZXJ2aWV3IGFib3V0IHRoZSBjb21wbGV0ZSBkYXRhDQogLSBTcGxpdCBkYXRhIGludG8gdHJhaW5pbmcgYW5kIHRlc3Qgc2V0IHVzaW5nIHN0cmF0aWZpZWQgc2FtcGxpbmcNCiAtIERpc2NvdmVyIGFuZCB2aXN1YWxpemUgdGhlIHRyYWluaW5nIGRhdGEgdG8gZ2FpbiBpbnNpZ2h0cw0KDQojIyMgU2V0dXANCklmIHlvdSBsaWtlIHRvIGluc3RhbGwgYWxsIHBhY2thZ2VzIGF0IG9uY2UsIHVzZSB0aGUgY29kZSBiZWxvdy4NCg0KYGBge3J9DQojaW5zdGFsbC5wYWNrYWdlcyhjKCJ0aWR5dmVyc2UiLCAic2tpbXIiLCAiR0dhbGx5IiwgImdnbWFwIiwgInZpc2RhdCIsICJjb3JyciIsICJnZ3NpZ25pZiIsICJndCIsICJ2aXAiLCAidGhlbWlzIiwgInB1cnJyIiwgInRpZHlyIiwgInRpZHltb2RlbHMiLCAia2VyYXMiLCAicmFuZ2VyIiwgInhnYm9vc3QiLCAia2tubiIpKSANCg0KbGlicmFyeSh0aWR5dmVyc2UpICAgIyBDb2xsZWN0aW9uIG9mIFIgcGFja2FnZXMgZm9yIGRhdGEgbWFuaXB1bGF0aW9uLCB2aXN1YWxpemF0aW9uLCBhbmQgYW5hbHlzaXMgIA0KbGlicmFyeShza2ltcikgICAgICAgIyBQcm92aWRlcyBlbmhhbmNlZCBzdW1tYXJ5IHN0YXRpc3RpY3MgZm9yIGRhdGEgZnJhbWVzICANCmxpYnJhcnkoR0dhbGx5KSAgICAgICMgRXh0ZW5zaW9uIG9mIGdncGxvdDIgZm9yIGNvcnJlbGF0aW9uIG1hdHJpY2VzLCBzY2F0dGVycGxvdCBtYXRyaWNlcywgYW5kIG1vcmUgIA0KbGlicmFyeShnZ21hcCkgICAgICAgIyBFbmFibGVzIHdvcmtpbmcgd2l0aCBzcGF0aWFsIGRhdGEgYW5kIG1hcHMgdXNpbmcgZ2dwbG90MiAgDQpsaWJyYXJ5KHZpc2RhdCkgICAgICAjIFZpc3VhbGl6ZXMgbWlzc2luZyB2YWx1ZXMgYW5kIGRhdGEgc3RydWN0dXJlIGZvciBleHBsb3JhdG9yeSBkYXRhIGFuYWx5c2lzICANCmxpYnJhcnkoY29ycnIpICAgICAgICMgRm9jdXNlZCBvbiBjb3JyZWxhdGlvbiBhbmFseXNpcyBhbmQgdmlzdWFsaXphdGlvbiAgDQpsaWJyYXJ5KGdnc2lnbmlmKSAgICAjIEFkZHMgc2lnbmlmaWNhbmNlIGluZGljYXRvcnMgKGUuZy4sIHAtdmFsdWVzKSB0byBnZ3Bsb3QyIHBsb3RzICANCmxpYnJhcnkoZ3QpICAgICAgICAgICMgQ3JlYXRlcyB2aXN1YWxseSBhcHBlYWxpbmcgdGFibGVzIGluIFIgIA0KbGlicmFyeSh2aXApICAgICAgICAgIyBHZW5lcmF0ZXMgdmFyaWFibGUgaW1wb3J0YW5jZSBwbG90cyBmb3IgbWFjaGluZSBsZWFybmluZyBtb2RlbHMgIA0KbGlicmFyeSh0aGVtaXMpICAgICAgIyBIZWxwcyB3aXRoIGRlYWxpbmcgd2l0aCBjbGFzcyBpbWJhbGFuY2UgaW4gbWFjaGluZSBsZWFybmluZyBkYXRhc2V0cyAgDQpsaWJyYXJ5KHB1cnJyKSAgICAgICAjIFByb3ZpZGVzIGZ1bmN0aW9uYWwgcHJvZ3JhbW1pbmcgdG9vbHMgZm9yIGl0ZXJhdGlvbiBhbmQgbWFwcGluZyAgDQpsaWJyYXJ5KHRpZHlyKSAgICAgICAjIEhlbHBzIHJlc2hhcGUgYW5kIHRpZHkgZGF0YSBmb3IgZWFzaWVyIGFuYWx5c2lzICANCmxpYnJhcnkodGlkeW1vZGVscykgICMgRnJhbWV3b3JrIGZvciBtb2RlbGluZyBhbmQgbWFjaGluZSBsZWFybmluZyB1c2luZyB0aWR5IHByaW5jaXBsZXMgIA0KbGlicmFyeShrZXJhcykgICAgICAgIyBJbnRlcmZhY2UgdG8gVGVuc29yRmxvdyBmb3IgZGVlcCBsZWFybmluZyBpbiBSICANCmxpYnJhcnkocmFuZ2VyKSAgICAgICMgRmFzdCBpbXBsZW1lbnRhdGlvbiBvZiByYW5kb20gZm9yZXN0cyBmb3IgY2xhc3NpZmljYXRpb24gYW5kIHJlZ3Jlc3Npb24gIA0KbGlicmFyeSh4Z2Jvb3N0KSAgICAgIyBPcHRpbWl6ZWQgZ3JhZGllbnQgYm9vc3RpbmcgZnJhbWV3b3JrIGZvciBwcmVkaWN0aXZlIG1vZGVsaW5nICANCmxpYnJhcnkoa2tubikgICAgICAgICMgSW1wbGVtZW50cyBrLU5lYXJlc3QgTmVpZ2hib3JzIChrTk4pIGNsYXNzaWZpY2F0aW9uIGFuZCByZWdyZXNzaW9uIA0KbGlicmFyeShkaXNjcmltKSAgICAgIyBGb3IgZGlzY3JpbV9saW5lYXIgY2xhc3NpZmljYXRpb24gbW9kZWwgc3BlY2lmaWNhdGlvbnMgdXNpbmcgdGhlIE1BU1MgZW5naW5lLg0KYGBgDQojIyMgSW1wb3J0IERhdGENCkZpcnN0IG9mIGFsbCwgbGV04oCZcyBpbXBvcnQgdGhlIGRhdGE6DQoNCg0KYGBge3J9DQpMSU5LIDwtICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20va2lyZW56L2RhdGFzZXRzL21hc3Rlci9ob3VzaW5nX3VuY2xlYW4uY3N2Ig0KaG91c2luZ19kZiA8LSByZWFkX2NzdihMSU5LKQ0KYGBgDQoNCiMjIyBDbGVhbiBkYXRhDQpUbyBnZXQgYSBmaXJzdCBpbXByZXNzaW9uIG9mIHRoZSBkYXRhIHdlIHRha2UgYSBsb29rIGF0IHRoZSB0b3AgNCByb3dzOg0KDQpgYGB7cn0NCmhvdXNpbmdfZGYgfD4NCiAgc2xpY2VfaGVhZChuID0gNCkgfD4NCiAgZ3QoKSAjIHByaW50IG91dHB1dCB1c2luZyBndA0KYGBgDQoNCk5vdGljZSB0aGUgdmFsdWVzIGluIHRoZSBmaXJzdCByb3cgb2YgdGhlIHZhcmlhYmxlcyBgaG91c2luZ19tZWRpYW5fYWdlYCBhbmQgYG1lZGlhbl9ob3VzZV92YWx1ZWAuIFdlIG5lZWQgdG8gcmVtb3ZlIHRoZSBzdHJpbmdzIOKAnHllYXJz4oCdIGFuZCDigJwk4oCdLiBUaGVyZWZvcmUsIHdlIHVzZSB0aGUgZnVuY3Rpb24gYHN0cl9yZW1vdmVfYWxsYCBmcm9tIHRoZSBgc3RyaW5ncmAgcGFja2FnZS4gU2luY2UgdGhlcmUgY291bGQgYmUgbXVsdGlwbGUgd3JvbmcgZW50cmllcyBvZiB0aGUgc2FtZSB0eXBlLCB3ZSBhcHBseSBvdXIgY29ycmVjdGlvbnMgdG8gYWxsIG9mIHRoZSByb3dzIG9mIHRoZSBjb3JyZXNwb25kaW5nIHZhcmlhYmxlOg0KDQpgYGB7cn0NCmhvdXNpbmdfZGYgPC0gDQogIGhvdXNpbmdfZGYgfD4NCiAgbXV0YXRlKA0KICAgIGhvdXNpbmdfbWVkaWFuX2FnZSA9IHN0cl9yZW1vdmVfYWxsKGhvdXNpbmdfbWVkaWFuX2FnZSwgIlt5ZWFyc10iKSwNCiAgICBtZWRpYW5faG91c2VfdmFsdWUgPSBzdHJfcmVtb3ZlX2FsbChtZWRpYW5faG91c2VfdmFsdWUsICJbJF0iKQ0KICApDQpgYGANCg0KV2UgZG9u4oCZdCBjb3ZlciB0aGUgcGhhc2Ugb2YgZGF0YSBjbGVhbmluZyBpbiBkZXRhaWwgaW4gdGhpcyB0dXRvcmlhbC4gSG93ZXZlciwgaW4gYSByZWFsIGRhdGEgc2NpZW5jZSBwcm9qZWN0LCBkYXRhIGNsZWFuaW5nIGlzIHVzdWFsbHkgYSB2ZXJ5IHRpbWUgY29uc3VtaW5nIHByb2Nlc3MuDQoNCiMjIyBGb3JtYXQgZGF0YQ0KTmV4dCwgd2UgdGFrZSBhIGxvb2sgYXQgdGhlIGRhdGEgc3RydWN0dXJlIGFuZCBjaGVjayB3ZXRoZXIgYWxsIGRhdGEgZm9ybWF0cyBhcmUgY29ycmVjdDogIA0KIC0gTnVtZXJpYyB2YXJpYWJsZXMgc2hvdWxkIGJlIGZvcm1hdHRlZCBhcyBpbnRlZ2VycyAoYGludGApIG9yIGRvdWJsZSBwcmVjaXNpb24gZmxvYXRpbmcgcG9pbnQgbnVtYmVycyAoYGRibGApLiAgDQogLSBDYXRlZ29yaWNhbCAobm9taW5hbCBhbmQgb3JkaW5hbCkgdmFyaWFibGVzIHNob3VsZCB1c3VhbGx5IGJlIGZvcm1hdHRlZCBhcyBmYWN0b3JzIChgZmN0YCkgYW5kIG5vdCBjaGFyYWN0ZXJzIChgY2hyYCkuIEVzcGVjaWFsbHksIGlmIHRoZXkgZG9u4oCZdCBoYXZlIG1hbnkgbGV2ZWxzLg0KDQpgYGB7cn0NCmdsaW1wc2UoaG91c2luZ19kZikNCmBgYA0KDQpUaGUgcGFja2FnZSB2aXNkYXQgaGVscHMgdXMgdG8gZXhwbG9yZSB0aGUgZGF0YSBjbGFzcyBzdHJ1Y3R1cmUgdmlzdWFsbHk6DQpgYGB7cn0NCnZpc19kYXQoaG91c2luZ19kZikNCmBgYA0KDQpXZSBjYW4gb2JzZXJ2ZSB0aGF0IHRoZSBudW1lcmljIHZhcmlhYmxlcyBgaG91c2luZ19tZWRpYV9hZ2VgIGFuZCBgbWVkaWFuX2hvdXNlX3ZhbHVlYCBhcmUgZGVjbGFyZWQgYXMgY2hhcmFjdGVycyAoYGNocmApIGluc3RlYWQgb2YgbnVtZXJpYy4gV2UgY2hvb3NlIHRvIGZvcm1hdCB0aGUgdmFyaWFibGVzIGFzIGRibCwgc2luY2UgdGhlIHZhbHVlcyBjb3VsZCBiZSBmbG9hdGluZy1wb2ludCBudW1iZXJzLg0KDQpGdXJ0aGVybW9yZSwgdGhlIGNhdGVnb3JpY2FsIHZhcmlhYmxlIGBvY2Vhbl9wcm94aW1pdHlgIGlzIGZvcm1hdHRlZCBhcyBjaGFyYWN0ZXIgaW5zdGVhZCBvZiBmYWN0b3IuIExldOKAmXMgdGFrZSBhIGxvb2sgYXQgdGhlIGxldmVscyBvZiB0aGUgdmFyaWFibGU6DQoNCmBgYHtyfQ0KaG91c2luZ19kZiB8Pg0KICBjb3VudChvY2Vhbl9wcm94aW1pdHksDQogICAgICAgIHNvcnQgPSBUUlVFKQ0KYGBgDQpUaGUgdmFyaWFibGUgaGFzIG9ubHkgNSBsZXZlbHMgYW5kIHRoZXJlZm9yZSBzaG91bGQgYmUgZm9ybWF0dGVkIGFzIGEgZmFjdG9yLg0KDQpOb3RlIHRoYXQgaXQgaXMgdXN1YWxseSBhIGdvb2QgaWRlYSB0byBmaXJzdCB0YWtlIGNhcmUgb2YgdGhlIG51bWVyaWNhbCB2YXJpYWJsZXMuIEFmdGVyd2FyZHMsIHdlIGNhbiBlYXNpbHkgY29udmVydCBhbGwgcmVtYWluaW5nIGNoYXJhY3RlciB2YXJpYWJsZXMgdG8gZmFjdG9ycyB1c2luZyB0aGUgZnVuY3Rpb24gYWNyb3NzIGZyb20gdGhlIGRwbHlyIHBhY2thZ2UgKHdoaWNoIGlzIHBhcnQgb2YgdGhlIHRpZHl2ZXJzZSkuDQoNCmBgYHtyfQ0KIyBjb252ZXJ0IHRvIG51bWVyaWMNCmhvdXNpbmdfZGYgPC0gDQogIGhvdXNpbmdfZGYgfD4NCiAgbXV0YXRlKA0KICAgIGhvdXNpbmdfbWVkaWFuX2FnZSA9IGFzLm51bWVyaWMoaG91c2luZ19tZWRpYW5fYWdlKSwNCiAgICBtZWRpYW5faG91c2VfdmFsdWUgPSBhcy5udW1lcmljKG1lZGlhbl9ob3VzZV92YWx1ZSkNCiAgKQ0KDQojIGNvbnZlcnQgYWxsIHJlbWFpbmluZyBjaGFyYWN0ZXIgdmFyaWFibGVzIHRvIGZhY3RvcnMgDQpob3VzaW5nX2RmIDwtIA0KICBob3VzaW5nX2RmIHw+DQogIG11dGF0ZShhY3Jvc3Mod2hlcmUoaXMuY2hhcmFjdGVyKSwgYXMuZmFjdG9yKSkNCmBgYA0KICANCiMjIyBNaXNzaW5nIGRhdGEgIA0KTm93IGxldOKAmXMgdHVybiBvdXIgYXR0ZW50aW9uIHRvIG1pc3NpbmcgZGF0YS4gTWlzc2luZyBkYXRhIGNhbiBiZSB2aWV3ZWQgd2l0aCB0aGUgZnVuY3Rpb24gYHZpc19taXNzYCBmcm9tIHRoZSBwYWNrYWdlIGB2aXNkYXRgLiBXZSBhcnJhbmdlIHRoZSBkYXRhIGJ5IGNvbHVtbnMgd2l0aCBtb3N0IG1pc3NpbmduZXNzOg0KDQpgYGB7cn0NCnZpc19taXNzKGhvdXNpbmdfZGYsIHNvcnRfbWlzcyA9IFRSVUUpDQpgYGANCg0KSGVyZSBhbiBhbHRlcm5hdGl2ZSBtZXRob2QgdG8gb2J0YWluIG1pc3NpbmcgZGF0YToNCg0KYGBge3J9DQppcy5uYShob3VzaW5nX2RmKSB8PiBjb2xTdW1zKCkNCmBgYA0KV2UgaGF2ZSBhIG1pc3NpbmcgcmF0ZSBvZiAwLjElICgyMDcgY2FzZXMpIGluIG91ciB2YXJpYWJsZSBgdG90YWxfYmVkcm9tc2AuIFRoaXMgY2FuIGNhdXNlIHByb2JsZW1zIGZvciBzb21lIGFsZ29yaXRobXMuIFdlIHdpbGwgdGFrZSBjYXJlIG9mIHRoaXMgaXNzdWUgZHVyaW5nIG91ciBkYXRhIHByZXBhcmF0aW9uIHBoYXNlLg0KDQojIyMgQ3JlYXRlIG5ldyB2YXJpYWJsZXMgIA0KT25lIHZlcnkgaW1wb3J0YW50IHRoaW5nIHlvdSBtYXkgd2FudCB0byBkbyBhdCB0aGUgYmVnaW5uaW5nIG9mIHlvdXIgZGF0YSBzY2llbmNlIHByb2plY3QgaXMgdG8gY3JlYXRlIG5ldyB2YXJpYWJsZSBjb21iaW5hdGlvbnMuIEZvciBleGFtcGxlOiAgDQogLSB0aGUgX3RvdGFsIG51bWJlciBvZiByb29tc18gaW4gYSBkaXN0cmljdCBpcyBub3QgdmVyeSB1c2VmdWwgaWYgeW91IGRvbuKAmXQga25vdyBob3cgbWFueSBob3VzZWhvbGRzIHRoZXJlIGFyZS4gV2hhdCB5b3UgcmVhbGx5IHdhbnQgaXMgdGhlIF9udW1iZXIgb2Ygcm9vbXMgcGVyIGhvdXNlaG9sZF8uICANCiAtIFNpbWlsYXJseSwgdGhlIHRvdGFsIG51bWJlciBvZiBiZWRyb29tcyBieSBpdHNlbGYgaXMgbm90IHZlcnkgdXNlZnVsOiB5b3UgcHJvYmFibHkgd2FudCB0byBjb21wYXJlIGl0IHRvIHRoZSBudW1iZXIgb2Ygcm9vbXMuICANCiAtIEFuZCB0aGUgX3BvcHVsYXRpb24gcGVyIGhvdXNlaG9sZF8gYWxzbyBzZWVtcyBsaWtlIGFuIGludGVyZXN0aW5nIGF0dHJpYnV0ZSBjb21iaW5hdGlvbiB0byBsb29rIGF0LiAgDQoNCkxldOKAmXMgY3JlYXRlIHRoZXNlIG5ldyBhdHRyaWJ1dGVzOg0KDQpgYGB7cn0NCmhvdXNpbmdfZGYgPC0gDQogIGhvdXNpbmdfZGYgfD4NCiAgbXV0YXRlKHJvb21zX3Blcl9ob3VzZWhvbGQgPSB0b3RhbF9yb29tcy9ob3VzZWhvbGRzLA0KICAgICAgICBiZWRyb29tc19wZXJfcm9vbSA9IHRvdGFsX2JlZHJvb21zL3RvdGFsX3Jvb21zLA0KICAgICAgICBwb3B1bGF0aW9uX3Blcl9ob3VzZWhvbGQgPSBwb3B1bGF0aW9uL2hvdXNlaG9sZHMpDQpgYGANCg0KRnVydGhlcm1vcmUsIGluIG91ciBleGFtcGxlIHdlIG5lZWQgdG8gY3JlYXRlIG91ciBkZXBlbmRlbnQgdmFyaWFibGUgYW5kIGRyb3AgdGhlIG9yaWdpbmFsIG51bWVyaWMgdmFyaWFibGUuDQoNCmBgYHtyfQ0KaG91c2luZ19kZiA8LSANCiAgaG91c2luZ19kZiB8Pg0KICBtdXRhdGUocHJpY2VfY2F0ZWdvcnkgPSBjYXNlX3doZW4oIA0KICAgIG1lZGlhbl9ob3VzZV92YWx1ZSA8IDE1MDAwMCB+ICJiZWxvdyIsDQogICAgbWVkaWFuX2hvdXNlX3ZhbHVlID49IDE1MDAwMCB+ICJhYm92ZSINCiAgICApKSB8Pg0KICBtdXRhdGUocHJpY2VfY2F0ZWdvcnkgPSBhcy5mYWN0b3IocHJpY2VfY2F0ZWdvcnkpKSB8Pg0KICBkcGx5cjo6c2VsZWN0KC1tZWRpYW5faG91c2VfdmFsdWUpICMgYXZvaWQgY29uZmxpY3RzIHdpdGggb3RoZXIgcGFja2FnZXMgbGlrZSBNQVNTDQpgYGANCg0KU2luY2Ugd2UgY3JlYXRlZCB0aGUgbmV3IGxhYmVsIGBwcmljZV9jYXRlZ29yeWAgZnJvbSB0aGUgdmFyaWFibGUgYG1lZGlhbl9ob3VzZV92YWx1ZWAgaXQgaXMgY3J1Y2lhbCB0aGF0IHdlIG5ldmVyIHVzZSB0aGUgdmFyaWFibGUgYG1lZGlhbl9ob3VzZV92YWx1ZWAgYXMgYSBwcmVkaWN0b3IgaW4gb3VyIG1vZGVscy4gVGhlcmVmb3JlIHdlIGRyb3AgaXQuDQoNClRha2UgYSBsb29rIGF0IG91ciBkZXBlbmRlbnQgdmFyaWFibGUgYW5kIGNyZWF0ZSBhIHRhYmxlIHdpdGggdGhlIHBhY2thZ2UgZ3QNCg0KYGBge3J9DQpob3VzaW5nX2RmIHw+DQogIGNvdW50KHByaWNlX2NhdGVnb3J5LCAjIGNvdW50IG9ic2VydmF0aW9ucw0KICAgICAgICBuYW1lID0iZGlzdHJpY3RzX3RvdGFsIikgfD4gIyBuYW1lIHRoZSBuZXcgdmFyaWFibGUgDQogIG11dGF0ZShwZXJjZW50ID0gZGlzdHJpY3RzX3RvdGFsL3N1bShkaXN0cmljdHNfdG90YWwpKSB8PiAjIGNhbGN1bGF0ZSBwZXJjZW50YWdlcw0KICBndCgpICMgY3JlYXRlIHRhYmxlDQpgYGANCg0KTGV04oCZcyBtYWtlIGEgbmljZSBsb29raW5nIHRhYmxlOg0KDQpgYGB7cn0NCmhvdXNpbmdfZGYgfD4NCiAgY291bnQocHJpY2VfY2F0ZWdvcnksIA0KICAgICAgICBuYW1lID0iZGlzdHJpY3RzX3RvdGFsIikgJT4lDQogIG11dGF0ZShwZXJjZW50ID0gZGlzdHJpY3RzX3RvdGFsL3N1bShkaXN0cmljdHNfdG90YWwpKjEwMCwNCiAgICAgICAgIHBlcmNlbnQgPSByb3VuZChwZXJjZW50LCAyKSkgJT4lDQogZ3QoKSAlPiUNCiAgdGFiX2hlYWRlcigNCiAgICB0aXRsZSA9ICJDYWxpZm9ybmlhIG1lZGlhbiBob3VzZSBwcmljZXMiLA0KICAgIHN1YnRpdGxlID0gIkRpc3RyaWN0cyBhYm92ZSBhbmQgYmVsb3cgMTUwLjAwMCQiDQogICkgJT4lDQogIGNvbHNfbGFiZWwoDQogICAgcHJpY2VfY2F0ZWdvcnkgPSAiUHJpY2UiLA0KICAgIGRpc3RyaWN0c190b3RhbCA9ICJEaXN0cmljdHMiLA0KICAgIHBlcmNlbnQgPSAiUGVyY2VudCINCiAgKSB8Pg0KICBmbXRfbnVtYmVyKA0KICAgIGNvbHVtbnMgPSB2YXJzKGRpc3RyaWN0c190b3RhbCksDQogICAgc3VmZml4aW5nID0gVFJVRQ0KICApIA0KYGBgDQoNCiMjIyBEYXRhIG92ZXJ2aWV3ICANCkFmdGVyIHdlIHRvb2sgY2FyZSBvZiBvdXIgZGF0YSBpc3N1ZXMsIHdlIGNhbiBvYnRhaW4gYSBkYXRhIHN1bW1hcnkgb2YgYWxsIG51bWVyaWNhbCBhbmQgY2F0ZWdvcmljYWwgYXR0cmlidXRlcyB1c2luZyBhIGZ1bmN0aW9uIGZyb20gdGhlIHBhY2thZ2UgYHNraW1yYDoNCg0KYGBge3J9DQpza2ltKGhvdXNpbmdfZGYpDQpgYGANCg0KV2UgaGF2ZSAyMDY0MCBvYnNlcnZhdGlvbnMgYW5kIDEzIGNvbHVtbnMgaW4gb3VyIGRhdGEuDQoNCiAtIFRoZSAqKnNkKiogY29sdW1uIHNob3dzIHRoZSBzdGFuZGFyZCBkZXZpYXRpb24sIHdoaWNoIG1lYXN1cmVzIGhvdyBkaXNwZXJzZWQgdGhlIHZhbHVlcyBhcmUuDQoNCiAtIFRoZSAqKnAwKiosICoqcDI1KiosICoqcDUwKiosICoqcDc1KiogYW5kICoqcDEwMCoqIGNvbHVtbnMgc2hvdyB0aGUgY29ycmVzcG9uZGluZyBwZXJjZW50aWxlczogYSBwZXJjZW50aWxlIGluZGljYXRlcyB0aGUgdmFsdWUgYmVsb3cgd2hpY2ggYSBnaXZlbiBwZXJjZW50YWdlIG9mIG9ic2VydmF0aW9ucyBpbiBhIGdyb3VwIG9mIG9ic2VydmF0aW9ucyBmYWxsLiBGb3IgZXhhbXBsZSwgMjUlIG9mIHRoZSBkaXN0cmljdHMgaGF2ZSBhIGBob3VzaW5nX21lZGlhbl9hZ2VgIGxvd2VyIHRoYW4gMTgsIHdoaWxlIDUwJSBhcmUgbG93ZXIgdGhhbiAyOSBhbmQgNzUlIGFyZSBsb3dlciB0aGFuIDM3LiBUaGVzZSBhcmUgb2Z0ZW4gY2FsbGVkIHRoZSAyNXRoIHBlcmNlbnRpbGUgKG9yIGZpcnN0IHF1YXJ0aWxlKSwgdGhlIG1lZGlhbiwgYW5kIHRoZSA3NXRoIHBlcmNlbnRpbGUuDQoNCiAtIEZ1cnRoZXIgbm90ZSB0aGF0IHRoZSAqKm1lZGlhbiBpbmNvbWUqKiBhdHRyaWJ1dGUgZG9lcyBub3QgbG9vayBsaWtlIGl0IGlzIGV4cHJlc3NlZCBpbiBVUyBkb2xsYXJzIChVU0QpLiBBY3R1YWxseSB0aGUgZGF0YSBoYXMgYmVlbiBzY2FsZWQgYW5kIGNhcHBlZCBhdCAxNSAoYWN0dWFsbHksIDE1LjAwMDEpIGZvciBoaWdoZXIgbWVkaWFuIGluY29tZXMsIGFuZCBhdCAwLjUgKGFjdHVhbGx5LCAwLjQ5OTkpIGZvciBsb3dlciBtZWRpYW4gaW5jb21lcy4gVGhlIG51bWJlcnMgcmVwcmVzZW50IHJvdWdobHkgdGVucyBvZiB0aG91c2FuZHMgb2YgZG9sbGFycyAoZS5nLiwgMyBhY3R1YWxseSBtZWFucyBhYm91dCAkMzAsMDAwKS4NCg0KQW5vdGhlciBxdWljayB3YXkgdG8gZ2V0IGFuIG92ZXJ2aWV3IG9mIHRoZSB0eXBlIG9mIGRhdGEgeW91IGFyZSBkZWFsaW5nIHdpdGggaXMgdG8gcGxvdCBhICoqaGlzdG9ncmFtKiogZm9yIGVhY2ggbnVtZXJpY2FsIGF0dHJpYnV0ZS4gQSBoaXN0b2dyYW0gc2hvd3MgdGhlIG51bWJlciBvZiBpbnN0YW5jZXMgKG9uIHRoZSB2ZXJ0aWNhbCBheGlzKSB0aGF0IGhhdmUgYSBnaXZlbiB2YWx1ZSByYW5nZSAob24gdGhlIGhvcml6b250YWwgYXhpcykuIFlvdSBjYW4gZWl0aGVyIHBsb3QgdGhpcyBvbmUgYXR0cmlidXRlIGF0IGEgdGltZSwgb3IgeW91IGNhbiB1c2UgZ2dzY2F0bWF0IGZyb20gdGhlIHBhY2thZ2UgR0dhbGx5IG9uIHRoZSB3aG9sZSBkYXRhc2V0IChhcyBzaG93biBpbiB0aGUgZm9sbG93aW5nIGNvZGUgZXhhbXBsZSksIGFuZCBpdCB3aWxsIHBsb3QgYSBoaXN0b2dyYW0gZm9yIGVhY2ggbnVtZXJpY2FsIGF0dHJpYnV0ZSBhcyB3ZWxsIGFzIGNvcnJlbGF0aW9uIGNvZWZmaWNpZW50cyAoUGVhcnNvbiBpcyB0aGUgZGVmYXVsdCkuIFdlIGp1c3Qgc2VsZWN0IHRoZSBtb3N0IHByb21pc2luZyB2YXJpYWJlbHMgZm9yIG91ciBwbG90Og0KDQpgYGB7cixjYWNoZT1UUlVFfQ0KaG91c2luZ19kZiB8Pg0KICBkcGx5cjo6c2VsZWN0KA0KICAgIGhvdXNpbmdfbWVkaWFuX2FnZSwgDQogICAgbWVkaWFuX2luY29tZSwgYmVkcm9vbXNfcGVyX3Jvb20sIHJvb21zX3Blcl9ob3VzZWhvbGQsIA0KICAgIHBvcHVsYXRpb25fcGVyX2hvdXNlaG9sZCkgfD4NCiAgZ2dzY2F0bWF0KGFscGhhID0gMC4yKQ0KYGBgDQoNCkFub3RoZXIgb3B0aW9uIGlzIHRvIHVzZSBnZ3BhaXJzLCB3aGVyZSB3ZSBldmVuIGNhbiBpbnRlZ3JhdGUgY2F0ZWdvcmljYWwgdmFyaWFibGVzIGxpa2Ugb3VyIGRlcGVuZGVudCB2YXJpYWJsZSBwcmljZV9jYXRlZ29yeSBhbmQgb2NlYW4gcHJveGltaXR5IGluIHRoZSBvdXRwdXQ6DQoNCmBgYHtyfQ0KaG91c2luZ19kZiB8Pg0KICBkcGx5cjo6c2VsZWN0KA0KICAgIGhvdXNpbmdfbWVkaWFuX2FnZSwgDQogICAgbWVkaWFuX2luY29tZSwgYmVkcm9vbXNfcGVyX3Jvb20sIHJvb21zX3Blcl9ob3VzZWhvbGQsIA0KICAgIHBvcHVsYXRpb25fcGVyX2hvdXNlaG9sZCwgb2NlYW5fcHJveGltaXR5LA0KICAgIHByaWNlX2NhdGVnb3J5KSB8Pg0KICBnZ3BhaXJzKCkNCmBgYA0KDQpUaGVyZSBhcmUgYSBmZXcgdGhpbmdzIHlvdSBtaWdodCBub3RpY2UgaW4gdGhlc2UgaGlzdG9ncmFtczoNCg0KIC0gVGhlIHZhcmlhYmxlcyBtZWRpYW4gaW5jb21lLCBob3VzaW5nIG1lZGlhbiBhZ2Ugd2VyZSBjYXBwZWQuDQoNCiAtIE5vdGUgdGhhdCBvdXIgYXR0cmlidXRlcyBoYXZlIHZlcnkgZGlmZmVyZW50IHNjYWxlcy4gV2Ugd2lsbCB0YWtlIGNhcmUgb2YgdGhpcyBpc3N1ZSBsYXRlciBpbiBkYXRhIHByZXBhcmF0aW9uLCB3aGVuIHdlIHVzZSBmZWF0dXJlIHNjYWxpbmcgKGRhdGEgbm9ybWFsaXphdGlvbikuDQoNCiAtIEZpbmFsbHksIG1hbnkgaGlzdG9ncmFtcyBhcmUgdGFpbC1oZWF2eTogdGhleSBleHRlbmQgbXVjaCBmYXJ0aGVyIHRvIHRoZSByaWdodCBvZiB0aGUgbWVkaWFuIHRoYW4gdG8gdGhlIGxlZnQuIFRoaXMgbWF5IG1ha2UgaXQgYSBiaXQgaGFyZGVyIGZvciBzb21lIE1hY2hpbmUgTGVhcm5pbmcgYWxnb3JpdGhtcyB0byBkZXRlY3QgcGF0dGVybnMuIFdlIHdpbGwgdHJhbnNmb3JtIHRoZXNlIGF0dHJpYnV0ZXMgbGF0ZXIgb24gdG8gaGF2ZSBtb3JlIGJlbGwtc2hhcGVkIGRpc3RyaWJ1dGlvbnMuIEZvciBvdXIgcmlnaHQtc2tld2VkIGRhdGEgKGkuZS4sIHRhaWwgaXMgb24gdGhlIHJpZ2h0LCBhbHNvIGNhbGxlZCBwb3NpdGl2ZSBza2V3KSwgY29tbW9uIHRyYW5zZm9ybWF0aW9ucyBpbmNsdWRlIHNxdWFyZSByb290IGFuZCBsb2cgKHdlIHdpbGwgdXNlIHRoZSBsb2cpLg0KIA0KIA0KIyMjIERhdGEgc3BsaXR0aW5nDQpCZWZvcmUgd2UgZ2V0IHN0YXJ0ZWQgd2l0aCBvdXIgaW4tZGVwdGggZGF0YSBleHBsb3JhdGlvbiwgbGV04oCZcyBzcGxpdCBvdXIgc2luZ2xlIGRhdGFzZXQgaW50byB0d286IGEgdHJhaW5pbmcgc2V0IGFuZCBhIHRlc3Rpbmcgc2V0LiBUaGUgdHJhaW5pbmcgZGF0YSB3aWxsIGJlIHVzZWQgdG8gZml0IG1vZGVscywgYW5kIHRoZSB0ZXN0aW5nIHNldCB3aWxsIGJlIHVzZWQgdG8gbWVhc3VyZSBtb2RlbCBwZXJmb3JtYW5jZS4gV2UgcGVyZm9ybSBkYXRhIGV4cGxvcmF0aW9uIG9ubHkgb24gdGhlIHRyYWluaW5nIGRhdGEuDQoNCkEgKip0cmFpbmluZyBkYXRhc2V0KiogaXMgYSBkYXRhc2V0IG9mIGV4YW1wbGVzIHVzZWQgZHVyaW5nIHRoZSBsZWFybmluZyBwcm9jZXNzIGFuZCBpcyB1c2VkIHRvIGZpdCB0aGUgbW9kZWxzLiBBICoqdGVzdCBkYXRhc2V0KiogaXMgYSBkYXRhc2V0IHRoYXQgaXMgaW5kZXBlbmRlbnQgb2YgdGhlIHRyYWluaW5nIGRhdGFzZXQgYW5kIGlzIHVzZWQgdG8gZXZhbHVhdGUgdGhlIHBlcmZvcm1hbmNlIG9mIHRoZSBmaW5hbCBtb2RlbC4gSWYgYSBtb2RlbCBmaXQgdG8gdGhlIHRyYWluaW5nIGRhdGFzZXQgYWxzbyBmaXRzIHRoZSB0ZXN0IGRhdGFzZXQgd2VsbCwgbWluaW1hbCBvdmVyZml0dGluZyBoYXMgdGFrZW4gcGxhY2UuIEEgYmV0dGVyIGZpdHRpbmcgb2YgdGhlIHRyYWluaW5nIGRhdGFzZXQgYXMgb3Bwb3NlZCB0byB0aGUgdGVzdCBkYXRhc2V0IHVzdWFsbHkgcG9pbnRzIHRvIG92ZXJmaXR0aW5nLg0KDQpJbiBvdXIgZGF0YSBzcGxpdCwgd2Ugd2FudCB0byBlbnN1cmUgdGhhdCB0aGUgdHJhaW5pbmcgYW5kIHRlc3Qgc2V0IGlzIHJlcHJlc2VudGF0aXZlIG9mIHRoZSBjYXRlZ29yaWVzIG9mIG91ciBkZXBlbmRlbnQgdmFyaWFibGUuDQpgYGB7cn0NCmhvdXNpbmdfZGYgfD4NCiAgZ2dwbG90KGFlcyhwcmljZV9jYXRlZ29yeSkpICsNCiAgZ2VvbV9iYXIoKSANCmBgYA0KDQpJbiBnZW5lcmFsLCB3ZSB3b3VsZCBsaWtlIHRvIGhhdmUgaW5zdGFuY2VzIGZvciBlYWNoIHN0cmF0dW0sIG9yIGVsc2UgdGhlIGVzdGltYXRlIG9mIGEgc3RyYXR1beKAmXMgaW1wb3J0YW5jZSBtYXkgYmUgYmlhc2VkLiBBIHN0cmF0dW0gKHBsdXJhbCBzdHJhdGEpIHJlZmVycyB0byBhIHN1YnNldCAocGFydCkgb2YgdGhlIHdob2xlIGRhdGEgZnJvbSB3aGljaCBpcyBiZWluZyBzYW1wbGVkLiBXZSBvbmx5IGhhdmUgdHdvIGNhdGVnb3JpZXMgaW4gb3VyIGRhdGEuDQoNClRvIGFjdHVhbGx5IHNwbGl0IHRoZSBkYXRhLCB3ZSBjYW4gdXNlIHRoZSBgcnNhbXBsZWAgcGFja2FnZSAoaW5jbHVkZWQgaW4gdGlkeW1vZGVscykgdG8gY3JlYXRlIGFuIG9iamVjdCB0aGF0IGNvbnRhaW5zIHRoZSBpbmZvcm1hdGlvbiBvbiBob3cgdG8gc3BsaXQgdGhlIGRhdGEgKHdoaWNoIHdlIGNhbGwgZGF0YV9zcGxpdCksIGFuZCB0aGVuIHR3byBtb3JlIHJzYW1wbGUgZnVuY3Rpb25zIHRvIGNyZWF0ZSBkYXRhIGZyYW1lcyBmb3IgdGhlIHRyYWluaW5nIGFuZCB0ZXN0aW5nIHNldHM6DQoNCmBgYHtyfQ0KIyBGaXggdGhlIHJhbmRvbSBudW1iZXJzIGJ5IHNldHRpbmcgdGhlIHNlZWQgDQojIFRoaXMgZW5hYmxlcyB0aGUgYW5hbHlzaXMgdG8gYmUgcmVwcm9kdWNpYmxlIA0Kc2V0LnNlZWQoMTIzKQ0KDQojIFB1dCAzLzQgb2YgdGhlIGRhdGEgaW50byB0aGUgdHJhaW5pbmcgc2V0IA0KZGF0YV9zcGxpdCA8LSBpbml0aWFsX3NwbGl0KGhvdXNpbmdfZGYsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJvcCA9IDMvNCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJhdGEgPSBwcmljZV9jYXRlZ29yeSkNCg0KIyBDcmVhdGUgZGF0YWZyYW1lcyBmb3IgdGhlIHR3byBzZXRzOg0KdHJhaW5fZGF0YSA8LSB0cmFpbmluZyhkYXRhX3NwbGl0KSANCnRlc3RfZGF0YSA8LSB0ZXN0aW5nKGRhdGFfc3BsaXQpDQpgYGANCg0KIyMjIERhdGEgZXhwbG9yYXRpb24NClRoZSBwb2ludCBvZiBkYXRhIGV4cGxvcmF0aW9uIGlzIHRvIGdhaW4gaW5zaWdodHMgdGhhdCB3aWxsIGhlbHAgeW91IHNlbGVjdCBpbXBvcnRhbnQgdmFyaWFibGVzIGZvciB5b3VyIG1vZGVsIGFuZCB0byBnZXQgaWRlYXMgZm9yIGZlYXR1cmUgZW5naW5lZXJpbmcgaW4gdGhlIGRhdGEgcHJlcGFyYXRpb24gcGhhc2UuIFVzdXNhbGx5LCBkYXRhIGV4cGxvcmF0aW9uIGlzIGFuIGl0ZXJhdGl2ZSBwcm9jZXNzOiBvbmNlIHlvdSBnZXQgYSBwcm90b3R5cGUgbW9kZWwgdXAgYW5kIHJ1bm5pbmcsIHlvdSBjYW4gYW5hbHl6ZSBpdHMgb3V0cHV0IHRvIGdhaW4gbW9yZSBpbnNpZ2h0cyBhbmQgY29tZSBiYWNrIHRvIHRoaXMgZXhwbG9yYXRpb24gc3RlcC4gSXQgaXMgaW1wb3J0YW50IHRvIG5vdGUgdGhhdCB3ZSBwZXJmb3JtIGRhdGEgZXhwbG9yYXRpb24gb25seSB3aXRoIG91ciB0cmFpbmluZyBkYXRhLg0KDQojIyMjIENyZWF0ZSBkYXRhIGNvcHkNCldlIGZpcnN0IG1ha2UgYSBjb3B5IG9mIHRoZSB0cmFpbmluZyBkYXRhIHNpbmNlIHdlIGRvbuKAmXQgd2FudCB0byBhbHRlciBvdXIgZGF0YSBkdXJpbmcgZGF0YSBleHBsb3JhdGlvbi4NCg0KYGBge3J9DQpkYXRhX2V4cGxvcmUgPC0gdHJhaW5fZGF0YQ0KYGBgDQoNCk5leHQsIHdlIHRha2UgYSBjbG9zZXIgbG9vayBhdCB0aGUgcmVsYXRpb25zaGlwcyBiZXR3ZWVuIG91ciB2YXJpYWJsZXMuIEluIHBhcnRpY3VsYXIsIHdlIGFyZSBpbnRlcmVzdGVkIGluIHRoZSByZWxhdGlvbnNoaXBzIGJldHdlZW4gdXIgZGVwZW5kZW50IHZhcmlhYmxlIHByaWNlX2NhdGVnb3J5IGFuZCBhbGwgb3RoZXIgdmFyaWFibGVzLiBUaGUgZ29hbCBpcyB0byBpZGVudGlmeSBwb3NzaWJsZSBwcmVkaWN0b3IgdmFyaWFibGVzIHdoaWNoIHdlIGNvdWxkIHVzZSBpbiBvdXIgbW9kZWxzIHRvIHByZWRpY3QgdGhlIHByaWNlX2NhdGVnb3J5Lg0KDQojIyMjIEdlb2dyYXBoaWNhbCBvdmVydmlldw0KU2luY2Ugb3VyIGRhdGEgaW5jbHVkZXMgaW5mb3JtYXRpb24gYWJvdXQgbG9uZ2l0dWRlIGFuZCBsYXRpdHVkZSwgd2Ugc3RhcnQgb3VyIGRhdGEgZXhwbG9yYXRpb24gd2l0aCB0aGUgY3JlYXRpb24gb2YgYSBnZW9ncmFwaGljYWwgc2NhdHRlcnBsb3Qgb2YgdGhlIGRhdGEgdG8gZ2V0IHNvbWUgZmlyc3QgaW5zaWdodHM6DQoNCmBgYHtyfQ0KZGF0YV9leHBsb3JlIHw+DQogIGdncGxvdChhZXMoeCA9IGxvbmdpdHVkZSwgeSA9IGxhdGl0dWRlKSkgKw0KICBnZW9tX3BvaW50KGNvbG9yID0gImNvcm5mbG93ZXJibHVlIikNCmBgYA0KDQpBIGJldHRlciB2aXN1YWxpemF0aW9uIHRoYXQgaGlnaGxpZ2h0cyBoaWdoLWRlbnNpdHkgYXJlYXMgKHdpdGggcGFyYW1ldGVyIGBhbHBoYSA9IDAuMWAgKToNCmBgYHtyfQ0KZGF0YV9leHBsb3JlIHw+DQogIGdncGxvdChhZXMoeCA9IGxvbmdpdHVkZSwgeSA9IGxhdGl0dWRlKSkgKw0KICBnZW9tX3BvaW50KGNvbG9yID0gImNvcm5mbG93ZXJibHVlIiwgYWxwaGEgPSAwLjEpIA0KYGBgDQoNCk92ZXJ2aWV3IGFib3V0IENhbGlmb3JuaWEgaG91c2luZyBwcmljZXM6ICANCiAtIHJlZCBpcyBleHBlbnNpdmUsICANCiAtIHRlYWwgaXMgY2hlYXAgYW5kICANCiAtIGxhcmdlciBjaXJjbGVzIGluZGljYXRlIGFyZWFzIHdpdGggYSBsYXJnZXIgcG9wdWxhdGlvbi4gIA0KDQpgYGB7cn0NCmRhdGFfZXhwbG9yZSB8Pg0KICBnZ3Bsb3QoYWVzKHggPSBsb25naXR1ZGUsIHkgPSBsYXRpdHVkZSkpICsNCiAgZ2VvbV9wb2ludChhZXMoc2l6ZSA9IHBvcHVsYXRpb24sIGNvbG9yID0gcHJpY2VfY2F0ZWdvcnkpLCANCiAgICAgICAgICAgICBhbHBoYSA9IDAuNCkNCmBgYA0KIyMjIyBOdW1lcmljYWwgdmFyaWFibGVzDQpXZSBjYW4gdXNlIGJveHBsb3RzIHRvIGNoZWNrLCBpZiB3ZSBhY3R1YWxseSBmaW5kIGRpZmZlcmVuY2VzIGluIG91ciBudW1lcmljIHZhcmlhYmxlcyBmb3IgdGhlIGRpZmZlcmVudCBsZXZlbHMgb2Ygb3VyIGRlcGVuZGVudCBjYXRlZ29yaWNhbCB2YXJpYWJsZToNCg0KYGBge3J9DQpkYXRhX2V4cGxvcmUgfD4NCiAgZ2dwbG90KGFlcyh4ID0gcHJpY2VfY2F0ZWdvcnksIHkgPSBtZWRpYW5faW5jb21lLCANCiAgICAgICAgICAgICBmaWxsID0gcHJpY2VfY2F0ZWdvcnksIGNvbG9yID0gcHJpY2VfY2F0ZWdvcnkpKSArDQogIGdlb21fYm94cGxvdChhbHBoYT0wLjQpIA0KYGBgDQoNCkxldGBzIGRlZmluZSBhIGZ1bmN0aW9uIGZvciB0aGlzIHRhc2sgdGhhdCBhY2NlcHRzIHN0cmluZ3MgYXMgaW5wdXRzIHNvIHdlIGRvbuKAmXQgaGF2ZSB0byBjb3B5IGFuZCBwYXN0ZSBvdXIgY29kZSBmb3IgZXZlcnkgcGxvdC4gTm90ZSB0aGF0IHdlIG9ubHkgaGF2ZSB0byBjaGFuZ2UgdGhlIOKAnHktdmFyaWFibGXigJ0gaW4gZXZlcnkgcGxvdC4NCg0KYGBge3J9DQpwcmludF9ib3hwbG90IDwtIGZ1bmN0aW9uKC55X3Zhcil7DQogIA0KICAjIGNvbnZlcnQgc3RyaW5ncyB0byB2YXJpYWJsZQ0KICB5X3ZhciA8LSBzeW0oLnlfdmFyKSANCiANCiAgIyB1bnF1b3RlIHZhcmlhYmxlcyB1c2luZyB7e319DQogIGRhdGFfZXhwbG9yZSB8Pg0KICBnZ3Bsb3QoYWVzKHggPSBwcmljZV9jYXRlZ29yeSwgeSA9IHt7eV92YXJ9fSwNCiAgICAgICAgICAgICBmaWxsID0gcHJpY2VfY2F0ZWdvcnksIGNvbG9yID0gcHJpY2VfY2F0ZWdvcnkpKSArDQogIGdlb21fYm94cGxvdChhbHBoYT0wLjQpIA0KICANCn0gIA0KYGBgDQoNCk9idGFpbiBhbGwgb2YgdGhlIG5hbWVzIG9mIHRoZSB5LXZhcmlhYmxlcyB3ZSB3YW50IHRvIHVzZSBmb3Igb3VyIHBsb3RzOg0KDQpgYGB7cn0NCnlfdmFyIDwtIA0KICBkYXRhX2V4cGxvcmUgfD4NCiAgZHBseXI6OnNlbGVjdCh3aGVyZShpcy5udW1lcmljKSwgLWxvbmdpdHVkZSwgLSBsYXRpdHVkZSkgfD4NCiAgdmFyaWFibGUubmFtZXMoKSAjIG9idGFpbiBuYW1lDQpgYGANCg0KVGhlIGBtYXBgIGZ1bmN0aW9uIGFwcGx5cyB0aGUgZnVuY3Rpb24gYHByaW50X2JveHBsb3RgIHRvIGVhY2ggZWxlbWVudCBvZiBvdXIgYXRvbWljIHZlY3RvciBgeV92YXJgIGFuZCByZXR1cm5zIHRoZSBhY2NvcmRpbmcgcGxvdDoNCg0KYGBge3J9DQptYXAoeV92YXIsIHByaW50X2JveHBsb3QpDQpgYGANCldlIGNhbiBvYnNlcnZlIGEgZGlmZmVyZW5jZSBpbiB0aGUgcHJpY2VfY2F0ZWdvcnk6DQoNCiAtIFRoZSBkaWZmZXJlbmNlcyBiZXR3ZWVuIG91ciB0d28gZ3JvdXBzIGFyZSBxdWl0ZSBzbWFsbCBmb3IgYGhvdXNpbmdfbWVkaWFuX2FnZWAsIGB0b3RhbF9yb29tYCwgYHRvdGFsX2JlZHJvb21zYCwgYHBvcHVsYXRpb25gIGFuZCBgaG91c2Vob2xkc2ANCg0KIC0gV2UgY2FuIG9ic2VydmUgYSBub3RpY2VhYmxlIGRpZmZlcmVuY2UgZm9yIG91ciB2YXJpYWJsZXMgYG1lZGlhbl9pbmNvbWVgIGFuZCBgYmVkcm9vbXNfcGVyX3Jvb21gDQoNCiAtIGBwb3B1bGF0aW9uX3Blcl9ob3VzZWhvbGRgIGFuZCBgcm9vbXNfcGVyX2hvdXNlaG9sZGAgaW5jbHVkZSBzb21lIGV4dHJlbWUgdmFsdWVzLiBXZSBmaXJzdCBuZWVkIHRvIGZpeCB0aGlzIGJlZm9yZSB3ZSBjYW4gcHJvY2VlZCB3aXRoIG91ciBpbnRlcnByZXRhdGlvbnMgZm9yIHRoaXMgdmFyaWFibGVzLiAgDQoNCkFnYWluLCBsZXTigJlzIHdyaXRlIGEgc2hvcnQgZnVuY3Rpb24gZm9yIHRoaXMgdGFzayBhbmQgZmlsdGVyIHNvbWUgb2YgdGhlIGV4dHJlbWUgY2FzZXMuIFdlIGNhbGwgdGhlIG5ldyBmdW5jdGlvbiBwcmludF9ib3hwbG90X291dDoNCg0KYGBge3J9DQpwcmludF9ib3hwbG90X291dCA8LSBmdW5jdGlvbigueV92YXJfb3V0KXsNCiAgDQogIHlfdmFyIDwtIHN5bSgueV92YXJfb3V0KSANCiANCiAgZGF0YV9leHBsb3JlIHw+DQogIGZpbHRlcihyb29tc19wZXJfaG91c2Vob2xkIDwgNTAsIHBvcHVsYXRpb25fcGVyX2hvdXNlaG9sZCA8IDIwKSB8Pg0KICBnZ3Bsb3QoYWVzKHggPSBwcmljZV9jYXRlZ29yeSwgeSA9IHt7eV92YXJ9fSwNCiAgICAgICAgICAgICBmaWxsID0gcHJpY2VfY2F0ZWdvcnksIGNvbG9yID0gcHJpY2VfY2F0ZWdvcnkpKSArDQogIGdlb21fYm94cGxvdChhbHBoYT0wLjQpIA0KICANCn0gDQoNCnlfdmFyX291dCA8LSANCiAgZGF0YV9leHBsb3JlIHw+DQogIGRwbHlyOjpzZWxlY3Qocm9vbXNfcGVyX2hvdXNlaG9sZCwgcG9wdWxhdGlvbl9wZXJfaG91c2Vob2xkKSB8Pg0KICB2YXJpYWJsZS5uYW1lcygpIA0KDQptYXAoeV92YXJfb3V0LCBwcmludF9ib3hwbG90X291dCkNCmBgYA0KDQpOb3cgd2UgYXJlIGFibGUgdG8gcmVjb2duaXplIGEgc21hbGwgZGlmZmVyZW5jZSBmb3IgcG9wdWxhdGlvbl9wZXJfaG91c2Vob2xkLiByb29tc19wZXJfaG91c2Vob2xkIG9uIHRoZSBvdGhlciBoYW5kIGlzIHF1aXRlIHNpbWlsYXIgZm9yIGJvdGggZ3JvdXBzLg0KDQpBZGRpdGlvbmFsbHksIHdlIGNhbiB1c2UgdGhlIGZ1bmN0aW9uIGdnc2NhdG1hdCB0byBjcmVhdGUgcGxvdHMgd2l0aCBvdXIgZGVwZW5kZW50IHZhcmlhYmxlIGFzIGNvbG9yIGNvbHVtbjoNCg0KYGBge3J9DQpkYXRhX2V4cGxvcmUgfD4NCiAgZHBseXI6OnNlbGVjdChwcmljZV9jYXRlZ29yeSwgbWVkaWFuX2luY29tZSwgYmVkcm9vbXNfcGVyX3Jvb20sIHJvb21zX3Blcl9ob3VzZWhvbGQsIA0KICAgICAgICAgcG9wdWxhdGlvbl9wZXJfaG91c2Vob2xkKSB8Pg0KICBnZ3NjYXRtYXQoY29sb3I9InByaWNlX2NhdGVnb3J5IiwgDQogICAgICAgICAgICBjb3JNZXRob2QgPSAic3BlYXJtYW4iLA0KICAgICAgICAgICAgYWxwaGE9MC4yKQ0KYGBgDQoNClRoZXJlIGFyZSBhIGZldyB0aGluZ3MgeW91IG1pZ2h0IG5vdGljZSBpbiB0aGVzZSBoaXN0b2dyYW1zOg0KDQogLSBOb3RlIHRoYXQgb3VyIGF0dHJpYnV0ZXMgaGF2ZSB2ZXJ5IGRpZmZlcmVudCBzY2FsZXMuIFdlIHdpbGwgdGFrZSBjYXJlIG9mIHRoaXMgaXNzdWUgbGF0ZXIgaW4gZGF0YSBwcmVwYXJhdGlvbiwgd2hlbiB3ZSB1c2UgZmVhdHVyZSBzY2FsaW5nIChkYXRhIG5vcm1hbGl6YXRpb24pLg0KDQogLSBUaGUgaGlzdG9ncmFtcyBhcmUgdGFpbC1oZWF2eTogdGhleSBleHRlbmQgbXVjaCBmYXJ0aGVyIHRvIHRoZSByaWdodCBvZiB0aGUgbWVkaWFuIHRoYW4gdG8gdGhlIGxlZnQuIFRoaXMgbWF5IG1ha2UgaXQgYSBiaXQgaGFyZGVyIGZvciBzb21lIE1hY2hpbmUgTGVhcm5pbmcgYWxnb3JpdGhtcyB0byBkZXRlY3QgcGF0dGVybnMuIFdlIHdpbGwgdHJhbnNmb3JtIHRoZXNlIGF0dHJpYnV0ZXMgbGF0ZXIgb24gdG8gaGF2ZSBtb3JlIGJlbGwtc2hhcGVkIGRpc3RyaWJ1dGlvbnMuIEZvciBvdXIgcmlnaHQtc2tld2VkIGRhdGEgKGkuZS4sIHRhaWwgaXMgb24gdGhlIHJpZ2h0LCBhbHNvIGNhbGxlZCBwb3NpdGl2ZSBza2V3KSwgY29tbW9uIHRyYW5zZm9ybWF0aW9ucyBpbmNsdWRlIHNxdWFyZSByb290IGFuZCBsb2cgKHdlIHdpbGwgdXNlIHRoZSBsb2cpLg0KDQpBcyBhIHJlc3VsdCBvZiBvdXIgZGF0YSBleHBsb3JhdGlvbiwgd2Ugd2lsbCBpbmNsdWRlIHRoZSBudW1lcmljYWwgdmFyaWFibGVzDQoNCiAtIGBtZWRpYW5faW5jb21lYCwNCiAtIGBiZWRyb29tc19wZXJfcm9vbWAgYW5kDQogLSBgcG9wdWxhdGlvbl9wZXJfaG91c2Vob2xkYCAgDQogDQphcyBwcmVkaWN0b3JzIGluIG91ciBtb2RlbC4NCg0KIyMjIyBDYXRlZ29yaWNhbCB2YXJpYWJsZXMNCk5vdyBsZXTigJlzIGFuYWx5emUgdGhlIHJlbGF0aW9uc2hpcCBiZXR3ZWVuIG91ciBjYXRlZ29yaWNhbCB2YXJpYWJsZXMgb2NlYW4gcHJveGltaXR5IGFuZCBwcmljZV9jYXRlZ29yeS4gV2Ugc3RhcnQgd2l0aCBhIHNpbXBsZSBjb3VudC4NCg0KYGBge3J9DQpkYXRhX2V4cGxvcmUgfD4NCiAgY291bnQocHJpY2VfY2F0ZWdvcnksIG9jZWFuX3Byb3hpbWl0eSkgfD4NCiAgZ3JvdXBfYnkocHJpY2VfY2F0ZWdvcnkpIHw+DQogIG11dGF0ZShwZXJjZW50ID0gbiAvIHN1bShuKSAqMTAwLA0KICAgICAgICAgcGVyY2VudCA9IHJvdW5kKHBlcmNlbnQsIDIpKSB8Pg0KICBndCgpIHw+DQogICAgdGFiX2hlYWRlcigNCiAgICB0aXRsZSA9ICJDYWxpZm9ybmlhIG1lZGlhbiBob3VzZSBwcmljZXMiLA0KICAgIHN1YnRpdGxlID0gIkRpc3RyaWN0cyBhYm92ZSBhbmQgYmVsb3cgMTUwLjAwMCQiDQogICkgfD4NCiAgY29sc19sYWJlbCgNCiAgICBvY2Vhbl9wcm94aW1pdHkgPSAiT2NlYW4gUHJveGltaXR5IiwNCiAgICBuID0gIkRpc3RyaWN0cyIsDQogICAgcGVyY2VudCA9ICJQZXJjZW50Ig0KICApIHw+DQogIGZtdF9udW1iZXIoDQogICAgY29sdW1ucyA9IHZhcnMobiksDQogICAgc3VmZml4aW5nID0gVFJVRQ0KICApIA0KYGBgDQoNClRoZSBmdW5jdGlvbiBnZW9tX2JpbjJkKCkgY3JlYXRzIGEgaGVhdG1hcCBieSBjb3VudGluZyB0aGUgbnVtYmVyIG9mIGNhc2VzIGluIGVhY2ggZ3JvdXAsIGFuZCB0aGVuIG1hcHBpbmcgdGhlIG51bWJlciBvZiBjYXNlcyB0byBlYWNoIHN1Ymdyb3Vi4oCZcyBmaWxsLg0KDQpgYGB7cn0NCmRhdGFfZXhwbG9yZSAlPiUNCiAgZ2dwbG90KGFlcyhwcmljZV9jYXRlZ29yeSwgb2NlYW5fcHJveGltaXR5KSkgKw0KICBnZW9tX2JpbjJkKCkgKw0KICBzY2FsZV9maWxsX2NvbnRpbnVvdXModHlwZSA9ICJ2aXJpZGlzIikgDQpgYGANCldlIGNhbiBvYnNlcnZlIHRoYXQgbW9zdCBkaXN0cmljdHMgd2l0aCBhIG1lZGlhbiBob3VzZSBwcmljZSBhYm92ZSAxNTAsMDAwIGhhdmUgYW4gb2NlYW4gcHJveGltaXR5IGJlbG93IDEgaG91ci4gT24gdGhlIG90aGVyIGhhbmQsIGRpc3RyaWN0cyBiZWxvdyB0aGF0IHRocmVzaG9sZCBhcmUgdHlwaWNhbGx5IGlubGFuZC4gSGVuY2UsIG9jZWFuIHByb3hpbWl0eSBpcyBpbmRlZWQgYSBnb29kIHByZWRpY3RvciBmb3Igb3VyIHR3byBkaWZmZXJlbnQgbWVkaWFuIGhvdXNlIHZhbHVlIGNhdGVnb3JpZXMuDQoNCiMjIERhdGEgcHJlcGFyYXRpb24NCg0KIC0gSGFuZGxlIG1pc3NpbmcgdmFsdWVzDQogLSBGaXggb3IgcmVtb3ZlIG91dGxpZXJzDQogLSBGZWF0dXJlIHNlbGVjdGlvbg0KIC0gRmVhdHVyZSBlbmdpbmVlcmluZw0KIC0gRmVhdHVyZSBzY2FsaW5nDQogLSBDcmVhdGUgYSB2YWxpZGF0aW9uIHNldA0KDQpOZXh0LCB3ZeKAmWxsIHByZXByb2Nlc3Mgb3VyIGRhdGEgYmVmb3JlIHRyYWluaW5nIHRoZSBtb2RlbHMuIFdlIG1haW5seSB1c2UgdGhlIHRpZHltb2RlbHMgcGFja2FnZXMgYHJlY2lwZXNgIGFuZCBgd29ya2Zsb3dzYCBmb3IgdGhlc2Ugc3RlcHMuIFJlY2lwZXMgYXJlIGJ1aWx0IGFzIGEgc2VyaWVzIG9mIG9wdGlvbmFsIGRhdGEgcHJlcGFyYXRpb24gc3RlcHMsIHN1Y2ggYXM6DQoNCiAtIF9EYXRhIGNsZWFuaW5nXzogRml4IG9yIHJlbW92ZSBvdXRsaWVycywgZmlsbCBpbiBtaXNzaW5nIHZhbHVlcyAoZS5nLiwgd2l0aCB6ZXJvLCBtZWFuLCBtZWRpYW7igKYpIG9yIGRyb3AgdGhlaXIgcm93cyAob3IgY29sdW1ucykuDQoNCiAtIF9GZWF0dXJlIHNlbGVjdGlvbl86IERyb3AgdGhlIGF0dHJpYnV0ZXMgdGhhdCBwcm92aWRlIG5vIHVzZWZ1bCBpbmZvcm1hdGlvbiBmb3IgdGhlIHRhc2suDQoNCiAtIF9GZWF0dXJlIGVuZ2luZWVyaW5nXzogRGlzY3JldGl6ZSBjb250aW51b3VzIGZlYXR1cmVzLCBkZWNvbXBvc2UgZmVhdHVyZXMgKGUuZy4sIHRoZSB3ZWVrZGF5IGZyb20gYSBkYXRlIHZhcmlhYmxlLCBldGMuKSwgYWRkIHByb21pc2luZyB0cmFuc2Zvcm1hdGlvbnMgb2YgZmVhdHVyZXMgKGUuZy4sIGxvZyh4KSwgc3FydCh4KSwgeDIgLCBldGMuKSBvciBhZ2dyZWdhdGUgZmVhdHVyZXMgaW50byBwcm9taXNpbmcgbmV3IGZlYXR1cmVzIChsaWtlIHdlIGFscmVhZHkgZGlkKS4NCg0KIC0gX0ZlYXR1cmUgc2NhbGluZ186IFN0YW5kYXJkaXplIG9yIG5vcm1hbGl6ZSBmZWF0dXJlcy4NCg0KV2Ugd2lsbCB3YW50IHRvIHVzZSBvdXIgcmVjaXBlIGFjcm9zcyBzZXZlcmFsIHN0ZXBzIGFzIHdlIHRyYWluIGFuZCB0ZXN0IG91ciBtb2RlbHMuIFRvIHNpbXBsaWZ5IHRoaXMgcHJvY2Vzcywgd2UgY2FuIHVzZSBhIGBtb2RlbCB3b3JrZmxvd2AsIHdoaWNoIHBhaXJzIGEgbW9kZWwgYW5kIHJlY2lwZSB0b2dldGhlci4NCg0KIyMjIERhdGEgcHJlcGFyYXRpb24gIA0KQmVmb3JlIHdlIGNyZWF0ZSBvdXIgYHJlY2lwZXNgLCB3ZSBmaXJzdCBzZWxlY3QgdGhlIHZhcmlhYmxlcyB3aGljaCB3ZSB3aWxsIHVzZSBpbiB0aGUgbW9kZWwuIE5vdGUgdGhhdCB3ZSBrZWVwIGBsb25naXR1ZGVgIGFuZCBgbGF0aXR1ZGVgIHRvIGJlIGFibGUgdG8gbWFwIHRoZSBkYXRhIGluIGEgbGF0ZXIgc3RhZ2UgYnV0IHdlIHdpbGwgbm90IHVzZSB0aGUgdmFyaWFibGVzIGluIG91ciBtb2RlbC4NCg0KYGBge3J9DQpob3VzaW5nX2RmX25ldyA8LQ0KICBob3VzaW5nX2RmIHw+DQogIGRwbHlyOjpzZWxlY3QoICMgc2VsZWN0IG91ciBwcmVkaWN0b3JzDQogICAgbG9uZ2l0dWRlLCBsYXRpdHVkZSwgDQogICAgcHJpY2VfY2F0ZWdvcnksIA0KICAgIG1lZGlhbl9pbmNvbWUsIA0KICAgIG9jZWFuX3Byb3hpbWl0eSwgDQogICAgYmVkcm9vbXNfcGVyX3Jvb20sIA0KICAgIHJvb21zX3Blcl9ob3VzZWhvbGQsIA0KICAgIHBvcHVsYXRpb25fcGVyX2hvdXNlaG9sZA0KICAgICAgICAgKQ0KDQpnbGltcHNlKGhvdXNpbmdfZGZfbmV3KQ0KYGBgDQoNCkZ1cnRoZXJtb3JlLCB3ZSBuZWVkIHRvIG1ha2UgYSBuZXcgZGF0YSBzcGxpdCBzaW5jZSB3ZSB1cGRhdGVkIHRoZSBvcmlnaW5hbCBkYXRhLg0KDQpgYGB7cn0NCnNldC5zZWVkKDEyMykNCg0KZGF0YV9zcGxpdCA8LSBpbml0aWFsX3NwbGl0KGhvdXNpbmdfZGZfbmV3LCAjIHVwZGF0ZWQgZGF0YQ0KICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJvcCA9IDMvNCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJhdGEgPSBwcmljZV9jYXRlZ29yeSkNCg0KdHJhaW5fZGF0YSA8LSB0cmFpbmluZyhkYXRhX3NwbGl0KSANCnRlc3RfZGF0YSA8LSB0ZXN0aW5nKGRhdGFfc3BsaXQpDQpgYGANCg0KDQojIyMgRGF0YSBwcmVwcm9wZWNlc3NpbmcgcmVjaXBlDQpUaGUgdHlwZSBvZiBkYXRhIHByZXByb2Nlc3NpbmcgaXMgZGVwZW5kZW50IG9uIHRoZSBkYXRhIGFuZCB0aGUgdHlwZSBvZiBtb2RlbCBiZWluZyBmaXQuIFRoZSBleGNlbGxlbnQgYm9vayDigJxUaWR5IE1vZGVsaW5nIHdpdGggUuKAnSBwcm92aWRlcyBhbiBbYXBwZW5kaXggd2l0aCByZWNvbW1lbmRhdGlvbnMgZm9yIGJhc2VsaW5lIGxldmVscyBvZiBwcmVwcm9jZXNzaW5nXShodHRwczovL3d3dy50bXdyLm9yZy9wcmUtcHJvYy10YWJsZS5odG1sKSB0aGF0IGFyZSBuZWVkZWQgZm9yIHZhcmlvdXMgbW9kZWwgZnVuY3Rpb25zLg0KDQpMZXTigJlzIGNyZWF0ZSBhIGJhc2UgYHJlY2lwZWAgZm9yIGFsbCBvZiBvdXIgY2xhc3NpZmljYXRpb24gbW9kZWxzLiBOb3RlIHRoYXQgdGhlIHNlcXVlbmNlIG9mIHN0ZXBzIG1hdHRlcjoNCg0KIC0gVGhlIGByZWNpcGUoKWAgZnVuY3Rpb24gaGFzIHR3byBhcmd1bWVudHM6DQoNCiAgICAxLiBBIGZvcm11bGEuIEFueSB2YXJpYWJsZSBvbiB0aGUgbGVmdC1oYW5kIHNpZGUgb2YgdGhlIHRpbGRlICh+KSBpcyBjb25zaWRlcmVkIHRoZSBtb2RlbCBvdXRjb21lIChoZXJlLCBgcHJpY2VfY2F0ZWdvcnlgKS4gT24gdGhlIHJpZ2h0LWhhbmQgc2lkZSBvZiB0aGUgdGlsZGUgYXJlIHRoZSBwcmVkaWN0b3JzLiBWYXJpYWJsZXMgbWF5IGJlIGxpc3RlZCBieSBuYW1lIChzZXBhcmF0ZWQgYnkgYSArKSwgb3IgeW91IGNhbiB1c2UgdGhlIGRvdCAoLikgdG8gaW5kaWNhdGUgYWxsIG90aGVyIHZhcmlhYmxlcyBhcyBwcmVkaWN0b3JzLiAgDQoNCiAgICAyLiBUaGUgZGF0YS4gQSByZWNpcGUgaXMgYXNzb2NpYXRlZCB3aXRoIHRoZSBkYXRhIHNldCB1c2VkIHRvIGNyZWF0ZSB0aGUgbW9kZWwuIFRoaXMgd2lsbCB0eXBpY2FsbHkgYmUgdGhlIHRyYWluaW5nIHNldCwgc28gYGRhdGEgPSB0cmFpbl9kYXRhYCBoZXJlLiAgDQoNCiAtIGB1cGRhdGVfcm9sZSgpYDogVGhpcyBzdGVwIG9mIGFkZGluZyByb2xlcyB0byBhIHJlY2lwZSBpcyBvcHRpb25hbDsgdGhlIHB1cnBvc2Ugb2YgdXNpbmcgaXQgaGVyZSBpcyB0aGF0IHRob3NlIHR3byB2YXJpYWJsZXMgY2FuIGJlIHJldGFpbmVkIGluIHRoZSBkYXRhIGJ1dCBub3QgaW5jbHVkZWQgaW4gdGhlIG1vZGVsLiBUaGlzIGNhbiBiZSBjb252ZW5pZW50IHdoZW4sIGFmdGVyIHRoZSBtb2RlbCBpcyBmaXQsIHdlIHdhbnQgdG8gaW52ZXN0aWdhdGUgc29tZSBwb29ybHkgcHJlZGljdGVkIHZhbHVlLiBUaGVzZSBJRCBjb2x1bW5zIHdpbGwgYmUgYXZhaWxhYmxlIGFuZCBjYW4gYmUgdXNlZCB0byB0cnkgdG8gdW5kZXJzdGFuZCB3aGF0IHdlbnQgd3JvbmcuDQoNCiAtIGBzdGVwX25hb21pdCgpYCByZW1vdmVzIG9ic2VydmF0aW9ucyAocm93cyBvZiBkYXRhKSBpZiB0aGV5IGNvbnRhaW4gYE5BYCBvciBgTmFOYCB2YWx1ZXMuIFdlIHVzZSBgc2tpcCA9IFRSVUVgIGJlY2F1c2Ugd2UgZG9u4oCZdCB3YW50IHRvIHBlcmZvcm0gdGhpcyBwYXJ0IHRvIG5ldyBkYXRhIHNvIHRoYXQgdGhlIG51bWJlciBvZiBzYW1wbGVzIGluIHRoZSBhc3Nlc3NtZW50IHNldCBpcyB0aGUgc2FtZSBhcyB0aGUgbnVtYmVyIG9mIHByZWRpY3RlZCB2YWx1ZXMgKGV2ZW4gaWYgdGhleSBhcmUgTkEpLg0KDQpOb3RlIHRoYXQgaW5zdGVhZCBvZiBkZWxldGluZyBtaXNzaW5nIHZhbHVlcyB3ZSBjb3VsZCBhbHNvIGVhc2lseSBzdWJzdGl0dXRlIChpLmUuLCBpbXB1dGUpIG1pc3NpbmcgdmFsdWVzIG9mIHZhcmlhYmxlcyBieSBvbmUgb2YgdGhlIGZvbGxvd2luZyBtZXRob2RzICh1c2luZyB0aGUgdHJhaW5pbmcgc2V0KToNCg0KIC0gW21lZGlhbl0oaHR0cHM6Ly9yZWNpcGVzLnRpZHltb2RlbHMub3JnL3JlZmVyZW5jZS9zdGVwX21lZGlhbmltcHV0ZS5odG1sKSwNCiAtIFttZWFuXShodHRwczovL3JlY2lwZXMudGlkeW1vZGVscy5vcmcvcmVmZXJlbmNlL3N0ZXBfbWVhbmltcHV0ZS5odG1sKSwNCiAtIFttb2RlXShodHRwczovL3JlY2lwZXMudGlkeW1vZGVscy5vcmcvcmVmZXJlbmNlL3N0ZXBfbW9kZWltcHV0ZS5odG1sKSwNCiAtIFtrLW5lYXJlc3QgbmVpZ2hib3JzXShodHRwczovL3JlY2lwZXMudGlkeW1vZGVscy5vcmcvcmVmZXJlbmNlL3N0ZXBfa25uaW1wdXRlLmh0bWwpLA0KIC0gW2xpbmVhciBtb2RlbF0oaHR0cHM6Ly9yZWNpcGVzLnRpZHltb2RlbHMub3JnL3JlZmVyZW5jZS9zdGVwX2ltcHV0ZV9saW5lYXIuaHRtbCksDQogLSBbYmFnZ2VkIHRyZWUgbW9kZWxzXShodHRwczovL3JlY2lwZXMudGlkeW1vZGVscy5vcmcvcmVmZXJlbmNlL3N0ZXBfYmFnaW1wdXRlLmh0bWwpICANCg0KVGFrZSBhIGxvb2sgYXQgdGhlIFtyZWNpcGVzIHJlZmVyZW5jZV0oaHR0cHM6Ly9yZWNpcGVzLnRpZHltb2RlbHMub3JnL3JlZmVyZW5jZS9pbmRleC5odG1sKSBmb3IgYW4gb3ZlcnZpZXcgYWJvdXQgYWxsIHBvc3NpYmxlIGltcHV0YXRpb24gbWV0aG9kcy4NCg0KIC0gYHN0ZXBfbm92ZWwoKWAgY29udmVydHMgYWxsIG5vbWluYWwgdmFyaWFibGVzIHRvIGZhY3RvcnMgYW5kIHRha2VzIGNhcmUgb2Ygb3RoZXIgaXNzdWVzIHJlbGF0ZWQgdG8gY2F0ZWdvcmljYWwgdmFyaWFibGVzLg0KDQogLSBgc3RlcF9sb2coKWAgd2lsbCBsb2cgdHJhbnNmb3JtIGRhdGEgKHNpbmNlIHNvbWUgb2Ygb3VyIG51bWVyaWNhbCB2YXJpYWJsZXMgYXJlIHJpZ2h0LXNrZXdlZCkuIE5vdGUgdGhhdCB0aGlzIHN0ZXAgY2FuIG5vdCBiZSBwZXJmb3JtZWQgb24gbmVnYXRpdmUgbnVtYmVycy4NCg0KIC0gYHN0ZXBfbm9ybWFsaXplKClgIG5vcm1hbGl6ZXMgKGNlbnRlciBhbmQgc2NhbGVzKSB0aGUgbnVtZXJpYyB2YXJpYWJsZXMgdG8gaGF2ZSBhIHN0YW5kYXJkIGRldmlhdGlvbiBvZiBvbmUgYW5kIGEgbWVhbiBvZiB6ZXJvLiAoaS5lLiwgei1zdGFuZGFyZGl6YXRpb24pLg0KDQogLSBgc3RlcF9kdW1teSgpYCBjb252ZXJ0cyBvdXIgZmFjdG9yIGNvbHVtbiBvY2Vhbl9wcm94aW1pdHkgaW50byBudW1lcmljIGJpbmFyeSAoMCBhbmQgMSkgdmFyaWFibGVzLg0KDQpOb3RlIHRoYXQgdGhpcyBzdGVwIG1heSBjYXVzZSBwcm9ibGVtcyBpZiB5b3VyIGNhdGVnb3JpY2FsIHZhcmlhYmxlIGhhcyB0b28gbWFueSBsZXZlbHMgLSBlc3BlY2lhbGx5IGlmIHNvbWUgb2YgdGhlIGxldmVscyBhcmUgdmVyeSBpbmZyZXF1ZW50LiBJbiB0aGlzIGNhc2UgeW91IHNob3VsZCBlaXRoZXIgZHJvcCB0aGUgdmFyaWFibGUgb3IgcG9vbCBpbmZyZXF1ZW50bHkgb2NjdXJyaW5nIHZhbHVlcyBpbnRvIGFuIOKAnG90aGVy4oCdIGNhdGVnb3J5IHdpdGggYFtzdGVwX290aGVyXShodHRwczovL3JlY2lwZXMudGlkeW1vZGVscy5vcmcvcmVmZXJlbmNlL3N0ZXBfb3RoZXIuaHRtbClgLiBUaGlzIHN0ZXBzIGhhcyB0byBiZSBwZXJmb3JtZWQgYmVmb3JlIGBzdGVwX2R1bW15YC4NCg0KIC0gYHN0ZXBfenYoKWA6IHJlbW92ZXMgYW55IG51bWVyaWMgdmFyaWFibGVzIHRoYXQgaGF2ZSB6ZXJvIHZhcmlhbmNlLg0KDQogLSBgc3RlcF9jb3JyKClgOiB3aWxsIHJlbW92ZSBwcmVkaWN0b3IgdmFyaWFibGVzIHRoYXQgaGF2ZSBsYXJnZSBjb3JyZWxhdGlvbnMgd2l0aCBvdGhlciBwcmVkaWN0b3IgdmFyaWFibGVzLg0KDQpOb3RlIHRoYXQgdGhlIHBhY2thZ2UgW3RoZW1pc10oaHR0cHM6Ly90aGVtaXMudGlkeW1vZGVscy5vcmcvKSBjb250YWlucyBleHRyYSBzdGVwcyBmb3IgdGhlIGByZWNpcGVzYCBwYWNrYWdlIGZvciBkZWFsaW5nIHdpdGggKippbWJhbGFuY2VkIGRhdGEqKi4gQSBjbGFzc2lmaWNhdGlvbiBkYXRhIHNldCB3aXRoIHNrZXdlZCBjbGFzcyBwcm9wb3J0aW9ucyBpcyBjYWxsZWQgaW1iYWxhbmNlZC4gQ2xhc3NlcyB0aGF0IG1ha2UgdXAgYSBsYXJnZSBwcm9wb3J0aW9uIG9mIHRoZSBkYXRhIHNldCBhcmUgY2FsbGVkIG1ham9yaXR5IGNsYXNzZXMuIFRob3NlIHRoYXQgbWFrZSB1cCBhIHNtYWxsZXIgcHJvcG9ydGlvbiBhcmUgbWlub3JpdHkgY2xhc3NlcyAoc2VlIFtHb29nbGUgRGV2ZWxvcGVyc10oaHR0cHM6Ly9kZXZlbG9wZXJzLmdvb2dsZS5jb20vbWFjaGluZS1sZWFybmluZy9kYXRhLXByZXAvY29uc3RydWN0L3NhbXBsaW5nLXNwbGl0dGluZy9pbWJhbGFuY2VkLWRhdGEpIGZvciBtb3JlIGRldGFpbHMpLiBgVGhlbWlzYCBwcm92aWRlcyB2YXJpb3VzIG1ldGhvZHMgZm9yIG92ZXItc2FtcGxpbmcgKGUuZy4gU01PVEUpIGFuZCB1bmRlci1zYW1wbGluZy4gSG93ZXZlciwgd2UgZG9u4oCZdCBoYXZlIHRvIHVzZSB0aGlzIG1ldGhvZHMgc2luY2Ugb3VyIGRhdGEgaXMgbm90IGltYmFsYW5jZWQuDQoNCmBgYHtyfQ0KaG91c2luZ19yZWMgPC0NCiAgcmVjaXBlKHByaWNlX2NhdGVnb3J5IH4gLiwNCiAgICAgICAgIGRhdGEgPSB0cmFpbl9kYXRhKSAlPiUNCiAgdXBkYXRlX3JvbGUobG9uZ2l0dWRlLCBsYXRpdHVkZSwgbmV3X3JvbGUgPSAiSUQiKSAlPiUNCiAgc3RlcF9sb2cobWVkaWFuX2luY29tZSwNCiAgICAgICAgICAgYmVkcm9vbXNfcGVyX3Jvb20sIHJvb21zX3Blcl9ob3VzZWhvbGQsIA0KICAgICAgICAgICBwb3B1bGF0aW9uX3Blcl9ob3VzZWhvbGQpICU+JQ0KICBzdGVwX25hb21pdChldmVyeXRoaW5nKCksIHNraXAgPSBUUlVFKSAlPiUNCiAgc3RlcF9ub3ZlbChhbGxfbm9taW5hbCgpLCAtYWxsX291dGNvbWVzKCkpICU+JQ0KICAjIE1ha2Ugc3VyZSBhbGwgbm9taW5hbCBwcmVkaWN0b3JzIChpbmNsdWRpbmcgb2NlYW5fcHJveGltaXR5KSBhcmUgZHVtbWllZA0KICBzdGVwX2R1bW15KGFsbF9ub21pbmFsX3ByZWRpY3RvcnMoKSwgLWFsbF9vdXRjb21lcygpKSAlPiUNCiAgc3RlcF9ub3JtYWxpemUoYWxsX251bWVyaWMoKSwgLWFsbF9vdXRjb21lcygpLCAtbG9uZ2l0dWRlLCAtbGF0aXR1ZGUpICU+JQ0KICBzdGVwX3p2KGFsbF9udW1lcmljKCksIC1hbGxfb3V0Y29tZXMoKSkgJT4lDQogIHN0ZXBfY29ycihhbGxfcHJlZGljdG9ycygpLCB0aHJlc2hvbGQgPSAwLjcsIG1ldGhvZCA9ICJzcGVhcm1hbiIpICU+JQ0KICBzdGVwX3Ntb3RlKHByaWNlX2NhdGVnb3J5KSANCmBgYA0KDQpUbyB2aWV3IHRoZSBjdXJyZW50IHNldCBvZiB2YXJpYWJsZXMgYW5kIHJvbGVzLCB1c2UgdGhlIGBzdW1tYXJ5KClgIGZ1bmN0aW9uOg0KDQpgYGB7cn0NCnN1bW1hcnkoaG91c2luZ19yZWMpDQpgYGANCklmIHdlIHdvdWxkIGxpa2UgdG8gY2hlY2sgaWYgYWxsIG9mIG91ciBwcmVwcm9jZXNzaW5nIHN0ZXBzIGZyb20gYWJvdmUgYWN0dWFsbHkgd29ya2VkLCB3ZSBjYW4gcHJvY2VlZCBhcyBmb2xsb3dzOg0KDQpgYGB7cn0NCnByZXBwZWRfZGF0YSA8LSANCiAgaG91c2luZ19yZWMgfD4jIHVzZSB0aGUgcmVjaXBlIG9iamVjdA0KICBwcmVwKCkgfD4jIHBlcmZvcm0gdGhlIHJlY2lwZSBvbiB0cmFpbmluZyBkYXRhDQogIGp1aWNlKCkgIyBleHRyYWN0IG9ubHkgdGhlIHByZXByb2Nlc3NlZCBkYXRhZnJhbWUgDQpgYGANCg0KVGFrZSBhIGxvb2sgYXQgdGhlIGRhdGEgc3RydWN0dXJlOg0KDQpgYGB7cn0NCmdsaW1wc2UocHJlcHBlZF9kYXRhKQ0KYGBgDQpWaXN1YWxpemUgdGhlIG51bWVyaWNhbCBkYXRhOg0KDQpgYGB7cn0NCnByZXBwZWRfZGF0YSB8Pg0KICBkcGx5cjo6c2VsZWN0KHByaWNlX2NhdGVnb3J5LCANCiAgICAgICAgIG1lZGlhbl9pbmNvbWUsIA0KICAgICAgICAgcm9vbXNfcGVyX2hvdXNlaG9sZCwgDQogICAgICAgICBwb3B1bGF0aW9uX3Blcl9ob3VzZWhvbGQpIHw+DQogIGdnc2NhdG1hdChjb3JNZXRob2QgPSAic3BlYXJtYW4iLA0KICAgICAgICAgICAgYWxwaGE9MC4yKQ0KYGBgDQpZb3Ugc2hvdWxkIG5vdGljZSB0aGF0Og0KDQogLSB0aGUgdmFyaWFibGVzIGBsb25naXR1ZGVgIGFuZCBgbGF0aXR1ZGVgIGRpZCBub3QgY2hhbmdlLg0KDQogLSBgbWVkaWFuX2luY29tZWAsIGByb29tc19wZXJfaG91c2Vob2xkYCBhbmQgYHBvcHVsYXRpb25fcGVyX2hvdXNlaG9sZGAgYXJlIG5vdyBfel8tc3RhbmRhcmRpemVkIGFuZCB0aGUgZGlzdHJpYnV0aW9ucyBhcmUgYSBiaXQgbGVzcyByaWdodCBza2V3ZWQgKGR1ZSB0byBvdXIgbG9nIHRyYW5zZm9ybWF0aW9uKQ0KDQogLSBgb2NlYW5fcHJveGltaXR5YCB3YXMgcmVwbGFjZWQgYnkgZHVtbXkgdmFyaWFibGVzLg0KDQojIyMjIFZhbGlkYXRpb24gc2V0ICANClJlbWVtYmVyIHRoYXQgd2UgYWxyZWFkeSBwYXJ0aXRpb25lZCBvdXIgZGF0YSBzZXQgaW50byBhIHRyYWluaW5nIHNldCBhbmQgdGVzdCBzZXQuIFRoaXMgbGV0cyB1cyBqdWRnZSB3aGV0aGVyIGEgZ2l2ZW4gbW9kZWwgd2lsbCBnZW5lcmFsaXplIHdlbGwgdG8gbmV3IGRhdGEuIEhvd2V2ZXIsIHVzaW5nIG9ubHkgdHdvIHBhcnRpdGlvbnMgbWF5IGJlIGluc3VmZmljaWVudCB3aGVuIGRvaW5nIG1hbnkgcm91bmRzIG9mIGh5cGVycGFyYW1ldGVyIHR1bmluZyAod2hpY2ggd2UgZG9u4oCZdCBwZXJmb3JtIGluIHRoaXMgdHV0b3JpYWwgYnV0IGl0IGlzIGFsd2F5cyByZWNvbW1lbmRlZCB0byB1c2UgYSB2YWxpZGF0aW9uIHNldCkuDQoNClRoZXJlZm9yZSwgaXQgaXMgdXN1YWxseSBhIGdvb2QgaWRlYSB0byBjcmVhdGUgYSBzbyBjYWxsZWQgdmFsaWRhdGlvbiBzZXQuIFdhdGNoIFt0aGlzIHNob3J0IHZpZGVvXShodHRwczovL2RldmVsb3BlcnMuZ29vZ2xlLmNvbS9tYWNoaW5lLWxlYXJuaW5nL2NyYXNoLWNvdXJzZS92YWxpZGF0aW9uL3ZpZGVvLWxlY3R1cmUpIGZyb20gR29vZ2xl4oCZcyBNYWNoaW5lIExlYXJuaW5nIGNyYXNoIGNvdXJzZSB0byBsZWFybiBtb3JlIGFib3V0IHRoZSB2YWx1ZSBvZiBhIHZhbGlkYXRpb24gc2V0Lg0KDQpXZSB1c2UgX2tfLWZvbGQgY3Jvc3MgdmFsaWRhdGlvbiB0byBidWlsZCBhIHNldCBvZiA1IHZhbGlkYXRpb24gZm9sZHMgd2l0aCB0aGUgZnVuY3Rpb24gYHZmb2xkX2N2YC4gV2UgYWxzbyB1c2Ugc3RyYXRpZmllZCBzYW1wbGluZzoNCg0KYGBge3J9DQpzZXQuc2VlZCgxMDApDQoNCmN2X2ZvbGRzIDwtDQogdmZvbGRfY3YodHJhaW5fZGF0YSwgDQogICAgICAgICAgdiA9IDUsIA0KICAgICAgICAgIHN0cmF0YSA9IHByaWNlX2NhdGVnb3J5KSANCmBgYA0KDQpXZSB3aWxsIGNvbWUgYmFjayB0byB0aGUgdmFsaWRhdGlvbiBzZXQgYWZ0ZXIgd2Ugc3BlY2lmaWVkIG91ciBtb2RlbHMuDQoNCiMjIE1vZGVsIGJ1aWxkaW5nICANCiMjIyBTcGVjaWZ5IG1vZGVscyAgDQpUaGUgcHJvY2VzcyBvZiBzcGVjaWZ5aW5nIG91ciBtb2RlbHMgaXMgYWx3YXlzIGFzIGZvbGxvd3M6ICANCg0KMS4gUGljayBhIG1vZGVsIHR5cGUgIA0KMi4gU2V0IHRoZSBlbmdpbmUgIA0KMy4gU2V0IHRoZSBgbW9kZWA6IHJlZ3Jlc3Npb24gb3IgY2xhc3NpZmljYXRpb24NCg0KWW91IGNhbiBjaG9vc2UgdGhlIG1vZGVsIHR5cGUgYW5kIGVuZ2luZSBmcm9tIHRoaXMgbGlzdC4NCg0KIyMjIyBMb2dpc3RpYyByZWdyZXNzaW9uDQpgYGB7cn0NCmxvZ19zcGVjIDwtICMgeW91ciBtb2RlbCBzcGVjaWZpY2F0aW9uDQogIGxvZ2lzdGljX3JlZygpIHw+ICMgbW9kZWwgdHlwZQ0KICBzZXRfZW5naW5lKGVuZ2luZSA9ICJnbG0iKSB8PiAjIG1vZGVsIGVuZ2luZQ0KICBzZXRfbW9kZSgiY2xhc3NpZmljYXRpb24iKSAjIG1vZGVsIG1vZGUNCg0KIyBTaG93IHlvdXIgbW9kZWwgc3BlY2lmaWNhdGlvbg0KbG9nX3NwZWMNCmBgYA0KDQojIyMjIExpbmVhciBkaXNjcmltaW5hbnQNCmBgYHtyfQ0KbGRhX3NwZWMgPC0NCiAgZGlzY3JpbV9saW5lYXIoKSB8Pg0KICBzZXRfZW5naW5lKGVuZ2luZT0nTUFTUycpIHw+DQogIHNldF9tb2RlKCJjbGFzc2lmaWNhdGlvbiIpDQoNCiMgU2hvdyB5b3VyIG1vZGVsIHNwZWNpZmljYXRpb24NCmxkYV9zcGVjDQpgYGANCg0KIyMjIyBRdWFkcmF0aWMgZGlzY3JpbWluYW50DQpgYGB7cn0NCnFkYV9zcGVjIDwtDQogIGRpc2NyaW1fcXVhZCgpIHw+DQogIHNldF9lbmdpbmUoZW5naW5lPSdNQVNTJykgfD4NCiAgc2V0X21vZGUoImNsYXNzaWZpY2F0aW9uIikNCg0KIyBTaG93IHlvdXIgbW9kZWwgc3BlY2lmaWNhdGlvbg0KcWRhX3NwZWMNCmBgYA0KIyMjIyBLLW5lYXJlc3QgbmVpZ2hib3INCmBgYHtyfQ0Ka25uX3NwZWMgPC0gDQogIG5lYXJlc3RfbmVpZ2hib3IobmVpZ2hib3JzID0gNCkgfD4jIHdlIGNhbiBhZGp1c3QgdGhlIG51bWJlciBvZiBuZWlnaGJvcnMgDQogIHNldF9lbmdpbmUoImtrbm4iKSB8Pg0KICBzZXRfbW9kZSgiY2xhc3NpZmljYXRpb24iKSANCmBgYA0KDQojIyMgQ3JlYXRlIHdvcmtmbG93cyAgDQpUbyBjb21iaW5lIHRoZSBkYXRhIHByZXBhcmF0aW9uIHJlY2lwZSB3aXRoIHRoZSBtb2RlbCBidWlsZGluZywgd2UgdXNlIHRoZSBwYWNrYWdlIFt3b3JrZmxvd3NdKGh0dHBzOi8vd29ya2Zsb3dzLnRpZHltb2RlbHMub3JnLykuIEEgd29ya2Zsb3cgaXMgYW4gb2JqZWN0IHRoYXQgY2FuIGJ1bmRsZSB0b2dldGhlciB5b3VyIHByZS1wcm9jZXNzaW5nIHJlY2lwZSwgbW9kZWxpbmcsIGFuZCBldmVuIHBvc3QtcHJvY2Vzc2luZyByZXF1ZXN0cyAobGlrZSBjYWxjdWxhdGluZyB0aGUgUk1TRSkuDQoNCiMjIyMgTG9naXN0aWMgcmVncmVzc2lvbiAgDQpCdW5kbGUgcmVjaXBlIGFuZCBtb2RlbCB3aXRoIHdvcmtmbG93czogIA0KDQpgYGB7cn0NCmxvZ193ZmxvdyA8LSAjIG5ldyB3b3JrZmxvdyBvYmplY3QNCiB3b3JrZmxvdygpIHw+IyB1c2Ugd29ya2Zsb3cgZnVuY3Rpb24NCiBhZGRfcmVjaXBlKGhvdXNpbmdfcmVjKSB8PiAgIyB1c2UgdGhlIG5ldyByZWNpcGUNCiBhZGRfbW9kZWwobG9nX3NwZWMpICAgIyBhZGQgeW91ciBtb2RlbCBzcGVjDQoNCiMgc2hvdyBvYmplY3QNCmxvZ193Zmxvdw0KYGBgDQoNCiMjIyMgTERBDQpCdW5kbGUgcmVjaXBlIGFuZCBtb2RlbDoNCg0KYGBge3J9DQpsZGFfd2Zsb3cgPC0gIyBuZXcgd29ya2Zsb3cgb2JqZWN0DQogd29ya2Zsb3coKSB8PiMgdXNlIHdvcmtmbG93IGZ1bmN0aW9uDQogYWRkX3JlY2lwZShob3VzaW5nX3JlYykgfD4gICMgdXNlIHRoZSBuZXcgcmVjaXBlDQogYWRkX21vZGVsKGxkYV9zcGVjKSAgICMgYWRkIHlvdXIgbW9kZWwgc3BlYw0KYGBgDQojIyMjIFFEQQ0KQnVuZGxlIHJlY2lwZSBhbmQgbW9kZWw6DQoNCmBgYHtyfQ0KcWRhX3dmbG93IDwtICMgbmV3IHdvcmtmbG93IG9iamVjdA0KIHdvcmtmbG93KCkgfD4jIHVzZSB3b3JrZmxvdyBmdW5jdGlvbg0KIGFkZF9yZWNpcGUoaG91c2luZ19yZWMpIHw+ICAjIHVzZSB0aGUgbmV3IHJlY2lwZQ0KIGFkZF9tb2RlbChxZGFfc3BlYykgICAjIGFkZCB5b3VyIG1vZGVsIHNwZWMNCmBgYA0KIyMjIyBLLW5lYXJlc3QgbmVpZ2hib3INCkJ1bmRsZSByZWNpcGUgYW5kIG1vZGVsOg0KDQpgYGB7cn0NCmtubl93ZmxvdyA8LQ0KIHdvcmtmbG93KCkgJT4lDQogYWRkX3JlY2lwZShob3VzaW5nX3JlYykgfD4NCiBhZGRfbW9kZWwoa25uX3NwZWMpDQpgYGANCg0KIyMjIEV2YWx1YXRlIG1vZGVscw0KTm93IHdlIGNhbiB1c2Ugb3VyIHZhbGlkYXRpb24gc2V0IChgY3ZfZm9sZHNgKSB0byBlc3RpbWF0ZSB0aGUgcGVyZm9ybWFuY2Ugb2Ygb3VyIG1vZGVscyB1c2luZyB0aGUgYGZpdF9yZXNhbXBsZXMoKWAgZnVuY3Rpb24gdG8gZml0IHRoZSBtb2RlbHMgb24gZWFjaCBvZiB0aGUgZm9sZHMgYW5kIHN0b3JlIHRoZSByZXN1bHRzLg0KDQpOb3RlIHRoYXQgYGZpdF9yZXNhbXBsZXMoKWAgd2lsbCBmaXQgb3VyIG1vZGVsIHRvIGVhY2ggcmVzYW1wbGUgYW5kIGV2YWx1YXRlIG9uIHRoZSBob2xkb3V0IHNldCBmcm9tIGVhY2ggcmVzYW1wbGUuIFRoZSBmdW5jdGlvbiBpcyB1c3VhbGx5IG9ubHkgdXNlZCBmb3IgY29tcHV0aW5nIHBlcmZvcm1hbmNlIG1ldHJpY3MgYWNyb3NzIHNvbWUgc2V0IG9mIHJlc2FtcGxlcyB0byBldmFsdWF0ZSBvdXIgbW9kZWxzIChsaWtlIGFjY3VyYWN5KSAtIHRoZSBtb2RlbHMgYXJlIG5vdCBldmVuIHN0b3JlZC4gSG93ZXZlciwgaW4gb3VyIGV4YW1wbGUgd2Ugc2F2ZSB0aGUgcHJlZGljdGlvbnMgaW4gb3JkZXIgdG8gdmlzdWFsaXplIHRoZSBtb2RlbCBmaXQgYW5kIHJlc2lkdWFscyB3aXRoIGBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKWAuDQoNCkZpbmFsbHksIHdlIGNvbGxlY3QgdGhlIHBlcmZvcm1hbmNlIG1ldHJpY3Mgd2l0aCBgY29sbGVjdF9tZXRyaWNzKClgIGFuZCBwaWNrIHRoZSBtb2RlbCB0aGF0IGRvZXMgYmVzdCBvbiB0aGUgdmFsaWRhdGlvbiBzZXQuDQoNCiMjIyMgTG9naXN0aWMgcmVncmVzc2lvbg0KV2UgdXNlIG91ciB3b3JrZmxvdyBvYmplY3QgdG8gcGVyZm9ybSByZXNhbXBsaW5nLiBGdXJ0aGVybW9yZSwgd2UgdXNlIGBtZXRyaWNfc2V0KClgIHRvIGNob29zZSBzb21lIGNvbW1vbiBjbGFzc2lmaWNhdGlvbiBwZXJmb3JtYW5jZSBtZXRyaWNzIHByb3ZpZGVkIGJ5IHRoZSBgeWFyZHN0aWNrYCBwYWNrYWdlLiBWaXNpdCBbeWFyZHN0aWNrcyByZWZlcmVuY2VdKGh0dHBzOi8veWFyZHN0aWNrLnRpZHltb2RlbHMub3JnL3JlZmVyZW5jZS9pbmRleC5odG1sKSB0byBzZWUgdGhlIGNvbXBsZXRlIGxpc3Qgb2YgYWxsIHBvc3NpYmxlIG1ldHJpY3MuDQoNCk5vdGUgdGhhdCBDb2hlbuKAmXMgX2thcHBhXyBjb2VmZmljaWVudCAoJFxrYXBwYSQpIGlzIGEgc2ltaWxhciBtZWFzdXJlIHRvIGFjY3VyYWN5LCBidXQgaXMgbm9ybWFsaXplZCBieSB0aGUgYWNjdXJhY3kgdGhhdCB3b3VsZCBiZSBleHBlY3RlZCBieSBjaGFuY2UgYWxvbmUgYW5kIGlzIHZlcnkgdXNlZnVsIHdoZW4gb25lIG9yIG1vcmUgY2xhc3NlcyBoYXZlIGxhcmdlIGZyZXF1ZW5jeSBkaXN0cmlidXRpb25zLiBUaGUgaGlnaGVyIHRoZSB2YWx1ZSwgdGhlIGJldHRlci4NCg0KYGBge3J9DQojIEVuc3VyZSB0aGF0IGFsbCBtZXRyaWMgZnVuY3Rpb25zIGFyZSBmcm9tIHlhcmRzdGljazoNCmxvZ19yZXMgPC0gDQogIGxvZ193ZmxvdyB8Pg0KICBmaXRfcmVzYW1wbGVzKA0KICAgIHJlc2FtcGxlcyA9IGN2X2ZvbGRzLCANCiAgICBtZXRyaWNzID0gbWV0cmljX3NldCgNCiAgICAgIHlhcmRzdGljazo6cmVjYWxsLCANCiAgICAgIHlhcmRzdGljazo6cHJlY2lzaW9uLCANCiAgICAgIHlhcmRzdGljazo6Zl9tZWFzLCANCiAgICAgIHlhcmRzdGljazo6YWNjdXJhY3ksIA0KICAgICAgeWFyZHN0aWNrOjprYXAsIA0KICAgICAgeWFyZHN0aWNrOjpyb2NfYXVjLCANCiAgICAgIHlhcmRzdGljazo6c2VucywgDQogICAgICB5YXJkc3RpY2s6OnNwZWMNCiAgICApLA0KICAgIGNvbnRyb2wgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKQ0KICApDQpgYGANCg0KIyMjIyMgTW9kZWwgY29lZmZpY2llbnRzDQpUaGUgYWJvdmUgZGVzY3JpYmVkIG1ldGhvZCB0byBvYnRhaW4gYGxvZ19yZXNgIGlzIGZpbmUgaWYgd2UgYXJlIG5vdCBpbnRlcmVzdGVkIGluIG1vZGVsIGNvZWZmaWNpZW50cy4gSG93ZXZlciwgaWYgd2Ugd291bGQgbGlrZSB0byBleHRyYWN0IHRoZSBtb2RlbCBjb2VmZmljaWVudHMgZnJvbSBgZml0X3Jlc2FtcGxlc2AsIHdlIG5lZWQgdG8gcHJvY2VlZCBhcyBmb2xsb3dzOg0KDQpgYGB7cn0NCiMgc2F2ZSBtb2RlbCBjb2VmZmljaWVudHMgZm9yIGEgZml0dGVkIG1vZGVsIG9iamVjdCBmcm9tIGEgd29ya2Zsb3cNCg0KZ2V0X21vZGVsIDwtIGZ1bmN0aW9uKHgpIHsNCiAgZXh0cmFjdF9maXRfcGFyc25pcCh4KSB8PnRpZHkoKQ0KfQ0KDQojIHNhbWUgYXMgYmVmb3JlIHdpdGggb25lIGV4Y2VwdGlvbg0KbG9nX3Jlc18yIDwtIA0KICBsb2dfd2Zsb3cgfD4NCiAgZml0X3Jlc2FtcGxlcygNCiAgICByZXNhbXBsZXMgPSBjdl9mb2xkcywgDQogICAgbWV0cmljcyA9IG1ldHJpY19zZXQoDQogICAgICB5YXJkc3RpY2s6OnJlY2FsbCwgDQogICAgICB5YXJkc3RpY2s6OnByZWNpc2lvbiwgDQogICAgICB5YXJkc3RpY2s6OmZfbWVhcywgDQogICAgICB5YXJkc3RpY2s6OmFjY3VyYWN5LCANCiAgICAgIHlhcmRzdGljazo6a2FwLCANCiAgICAgIHlhcmRzdGljazo6cm9jX2F1YywgDQogICAgICB5YXJkc3RpY2s6OnNlbnMsIA0KICAgICAgeWFyZHN0aWNrOjpzcGVjDQogICAgKSwNCiAgICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoDQogICAgICBzYXZlX3ByZWQgPSBUUlVFLA0KICAgICAgZXh0cmFjdCA9IGdldF9tb2RlbCkgIyB1c2UgZXh0cmFjdCBhbmQgb3VyIG5ldyBmdW5jdGlvbg0KICAgICkgDQpgYGANCg0KTm93IHRoZXJlIGlzIGEgYC5leHRyYWN0c2AgY29sdW1uIHdpdGggbmVzdGVkIHRpYmJsZXMuDQoNCmBgYHtyfQ0KbG9nX3Jlc18yJC5leHRyYWN0c1tbMV1dDQpgYGANCg0KVG8gZ2V0IHRoZSByZXN1bHRzIHVzZToNCg0KYGBge3J9DQpsb2dfcmVzXzIkLmV4dHJhY3RzW1sxXV1bWzFdXQ0KYGBgDQoNCkFsbCBvZiB0aGUgcmVzdWx0cyBjYW4gYmUgZmxhdHRlbmVkIGFuZCBjb2xsZWN0ZWQgdXNpbmc6DQoNCmBgYHtyfQ0KYWxsX2NvZWYgPC0gbWFwX2Rmcihsb2dfcmVzXzIkLmV4dHJhY3RzLCB+IC54W1sxXV1bWzFdXSkNCmBgYA0KDQpTaG93IGFsbCBvZiB0aGUgcmVzYW1wbGUgY29lZmZpY2llbnRzIGZvciBhIHNpbmdsZSBwcmVkaWN0b3I6DQoNCmBgYHtyfQ0KZmlsdGVyKGFsbF9jb2VmLCB0ZXJtID09ICJtZWRpYW5faW5jb21lIikNCmBgYA0KDQojIyMjIyBQZXJmb3JtYW5jZSBtZXRyaWNzDQpTaG93IGF2ZXJhZ2UgcGVyZm9ybWFuY2Ugb3ZlciBhbGwgZm9sZHMgKG5vdGUgdGhhdCB3ZSB1c2UgYGxvZ19yZXNgKToNCg0KYGBge3J9DQpsb2dfcmVzIHw+IGNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemUgPSBUUlVFKQ0KYGBgDQpTaG93IHBlcmZvcm1hbmNlIGZvciBldmVyeSBzaW5nbGUgZm9sZDoNCg0KYGBge3J9DQpsb2dfcmVzIHw+IGNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemUgPSBGQUxTRSkNCmBgYA0KDQojIyMjIyBDb2xsZWN0IHByZWRpY3Rpb25zDQpUbyBvYnRhaW4gdGhlIGFjdHVhbCBtb2RlbCBwcmVkaWN0aW9ucywgd2UgdXNlIHRoZSBmdW5jdGlvbiBjb2xsZWN0X3ByZWRpY3Rpb25zIGFuZCBzYXZlIHRoZSByZXN1bHQgYXMgYGxvZ19wcmVkYDoNCg0KYGBge3J9DQpsb2dfcHJlZCA8LSANCiAgbG9nX3JlcyAlPiUNCiAgY29sbGVjdF9wcmVkaWN0aW9ucygpDQpgYGANCg0KIyMjIyMgQ29uZnVzaW9uIG1hdHJpeA0KTm93IHdlIGNhbiB1c2UgdGhlIHByZWRpY3Rpb25zIHRvIGNyZWF0ZSBhIGNvbmZ1c2lvbiBtYXRyaXggd2l0aCBjb25mX21hdCgpOg0KDQpgYGB7cn0NCmxvZ19wcmVkIHw+DQogIGNvbmZfbWF0KHByaWNlX2NhdGVnb3J5LCAucHJlZF9jbGFzcykgDQpgYGANCkFkZGl0aW9uYWxseSwgdGhlIGNvbmZ1c2lvbiBtYXRyaXggY2FuIHF1aWNrbHkgYmUgdmlzdWFsaXplZCBpbiBkaWZmZXJlbnQgZm9ybWF0cyB1c2luZyBhdXRvcGxvdCgpLiBUeXBlIG1vc2FpYzoNCg0KYGBge3J9DQpsb2dfcHJlZCB8Pg0KICBjb25mX21hdChwcmljZV9jYXRlZ29yeSwgLnByZWRfY2xhc3MpIHw+DQogIGF1dG9wbG90KHR5cGUgPSAibW9zYWljIikNCmBgYA0KT3IgdHlwZSBoZWF0bWFwOg0KDQpgYGB7cn0NCmxvZ19wcmVkIHw+DQogIGNvbmZfbWF0KHByaWNlX2NhdGVnb3J5LCAucHJlZF9jbGFzcykgfD4NCiAgYXV0b3Bsb3QodHlwZSA9ICJoZWF0bWFwIikNCmBgYA0KIyMjIyMgUk9DLUN1cnZlDQpXZSBjYW4gYWxzbyBtYWtlIGFuIFJPQyBjdXJ2ZSBmb3Igb3VyIDUgZm9sZHMuIFNpbmNlIHRoZSBjYXRlZ29yeSB3ZSBhcmUgcHJlZGljdGluZyBpcyB0aGUgZmlyc3QgbGV2ZWwgaW4gdGhlIGBwcmljZV9jYXRlZ29yeWAgZmFjdG9yICjigJxhYm92ZeKAnSksIHdlIHByb3ZpZGUgYHJvY19jdXJ2ZSgpYCB3aXRoIHRoZSByZWxldmFudCBjbGFzcyBwcm9iYWJpbGl0eSBgLnByZWRfYWJvdmVgOg0KDQpgYGB7cn0NCmxvZ19wcmVkIHw+DQogIGdyb3VwX2J5KGlkKSB8PiMgaWQgY29udGFpbnMgb3VyIGZvbGRzDQogIHJvY19jdXJ2ZShwcmljZV9jYXRlZ29yeSwgLnByZWRfYWJvdmUpIHw+DQogIGF1dG9wbG90KCkNCmBgYA0KVmlzaXQgR29vZ2xlIGRldmVsb3BlcuKAmXMgW01hY2hpbmUgTGVhcm5pbmcgQ3Jhc2hjb3Vyc2VdKGh0dHBzOi8vZGV2ZWxvcGVycy5nb29nbGUuY29tL21hY2hpbmUtbGVhcm5pbmcvY3Jhc2gtY291cnNlL2NsYXNzaWZpY2F0aW9uL3JvYy1hbmQtYXVjKSB0byBsZWFybiBtb3JlIGFib3V0IHRoZSBST0MtQ3VydmUuDQoNCiMjIyMjIFByb2JhYmlsaXR5IGRpc3RyaWJ1dGlvbnMNClBsb3QgcHJlZGljdGVkIHByb2JhYmlsaXR5IGRpc3RyaWJ1dGlvbnMgZm9yIG91ciB0d28gY2xhc3Nlcy4NCg0KYGBge3J9DQpsb2dfcHJlZCB8Pg0KICBnZ3Bsb3QoKSArDQogIGdlb21fZGVuc2l0eShhZXMoeCA9IC5wcmVkX2Fib3ZlLCANCiAgICAgICAgICAgICAgICAgICBmaWxsID0gcHJpY2VfY2F0ZWdvcnkpLCANCiAgICAgICAgICAgICAgIGFscGhhID0gMC41KQ0KYGBgDQojIyMjIFJhbmRvbSBmb3Jlc3QNCldlIGRvbuKAmXQgcmVwZWF0IGFsbCBvZiB0aGUgc3RlcHMgc2hvd24gaW4gbG9naXN0aWMgcmVncmVzc2lvbiBhbmQganVzdCBmb2N1cyBvbiB0aGUgcGVyZm9ybWFuY2UgbWV0cmljcy4NCmBgYHtyfQ0KbGRhX3JlcyA8LQ0KICBsZGFfd2Zsb3cgfD4NCiAgZml0X3Jlc2FtcGxlcygNCiAgICByZXNhbXBsZXMgPSBjdl9mb2xkcywgDQogICAgbWV0cmljcyA9IG1ldHJpY19zZXQoDQogICAgICB5YXJkc3RpY2s6OnJlY2FsbCwgDQogICAgICB5YXJkc3RpY2s6OnByZWNpc2lvbiwgDQogICAgICB5YXJkc3RpY2s6OmZfbWVhcywgDQogICAgICB5YXJkc3RpY2s6OmFjY3VyYWN5LCANCiAgICAgIHlhcmRzdGljazo6a2FwLCANCiAgICAgIHlhcmRzdGljazo6cm9jX2F1YywgDQogICAgICB5YXJkc3RpY2s6OnNlbnMsIA0KICAgICAgeWFyZHN0aWNrOjpzcGVjDQogICAgKSwNCiAgICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXMoc2F2ZV9wcmVkID0gVFJVRSkNCiAgICApIA0KDQpsZGFfcmVzIHw+IGNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemUgPSBUUlVFKQ0KYGBgDQoNCiMjIyMgUURBDQpXZSBkb27igJl0IHJlcGVhdCBhbGwgb2YgdGhlIHN0ZXBzIHNob3duIGluIGxvZ2lzdGljIHJlZ3Jlc3Npb24gYW5kIGp1c3QgZm9jdXMgb24gdGhlIHBlcmZvcm1hbmNlIG1ldHJpY3MuDQoNCmBgYHtyLCBldmFsPUZBTFNFfQ0KcWRhX3JlcyA8LSANCiAgcWRhX3dmbG93IHw+DQogIGZpdF9yZXNhbXBsZXMoDQogICAgcmVzYW1wbGVzID0gY3ZfZm9sZHMsIA0KICAgIG1ldHJpY3MgPSBtZXRyaWNfc2V0KA0KICAgICAgeWFyZHN0aWNrOjpyZWNhbGwsIA0KICAgICAgeWFyZHN0aWNrOjpwcmVjaXNpb24sIA0KICAgICAgeWFyZHN0aWNrOjpmX21lYXMsIA0KICAgICAgeWFyZHN0aWNrOjphY2N1cmFjeSwgDQogICAgICB5YXJkc3RpY2s6OmthcCwgDQogICAgICB5YXJkc3RpY2s6OnJvY19hdWMsIA0KICAgICAgeWFyZHN0aWNrOjpzZW5zLCANCiAgICAgIHlhcmRzdGljazo6c3BlYw0KICAgICksDQogICAgY29udHJvbCA9IGNvbnRyb2xfcmVzYW1wbGVzKHNhdmVfcHJlZCA9IFRSVUUpDQogICAgKSANCg0KcWRhX3JlcyB8PmNvbGxlY3RfbWV0cmljcyhzdW1tYXJpemUgPSBUUlVFKQ0KDQoj4oaSIEEgfCBlcnJvcjogICByYW5rIGRlZmljaWVuY3kgaW4gZ3JvdXAgYmVsb3cNCiNUaGVyZSB3ZXJlIGlzc3VlcyB3aXRoIHNvbWUgY29tcHV0YXRpb25zICAgQTogeDUNCiNXYXJuaW5nOiBBbGwgbW9kZWxzIGZhaWxlZC4gUnVuIGBzaG93X25vdGVzKC5MYXN0LnR1bmUucmVzdWx0KWAgZm9yIG1vcmUgaW5mb3JtYXRpb24uRXJyb3IgaW4gYGVzdGltYXRlX3R1bmVfcmVzdWx0cygpYDoNCiMhIEFsbCBtb2RlbHMgZmFpbGVkLiBSdW4gYHNob3dfbm90ZXMoLkxhc3QudHVuZS5yZXN1bHQpYCBmb3IgbW9yZSBpbmZvcm1hdGlvbi4NCiNSdW4gYHJsYW5nOjpsYXN0X3RyYWNlKClgIHRvIHNlZSB3aGVyZSB0aGUgZXJyb3Igb2NjdXJyZWQuDQpgYGANCg0KVGhlIGVycm9yIG1lc3NhZ2Ugc3VnZ2VzdHMgdGhhdCB0aGVyZSBpcyBhIHJhbmsgZGVmaWNpZW5jeSBpbiB5b3VyIGBxZGFfcmVzYCBtb2RlbCwgbWVhbmluZyB0aGF0IHNvbWUgb2YgdGhlIHByZWRpY3RvciB2YXJpYWJsZXMgYXJlIGxpa2VseSBjb2xsaW5lYXIgKGhpZ2hseSBjb3JyZWxhdGVkKSBvciBoYXZlIG5lYXItemVybyB2YXJpYW5jZSwgd2hpY2ggaXMgcHJvYmxlbWF0aWMgZm9yIFF1YWRyYXRpYyBEaXNjcmltaW5hbnQgQW5hbHlzaXMgKFFEQSkuIEhlcmXigJlzIGhvdyB5b3UgY2FuIHRyb3VibGVzaG9vdCBhbmQgcmVzb2x2ZSB0aGUgaXNzdWU6ICANCg0KX18xLiBDaGVjayBmb3IgTmVhci1aZXJvIFZhcmlhbmNlIFByZWRpY3RvcnNfXyAgDQpRREEgZG9lcyBub3QgaGFuZGxlIHZhcmlhYmxlcyB3aXRoIG5lYXItemVybyB2YXJpYW5jZSB3ZWxsLiBZb3UgY2FuIGNoZWNrIGZvciBzdWNoIHByZWRpY3RvcnMgdXNpbmc6ICANCg0KYGBge3J9DQpsaWJyYXJ5KGNhcmV0KQ0KbmVhclplcm9WYXIodHJhaW5fZGF0YSwgc2F2ZU1ldHJpY3MgPSBUUlVFKQ0KYGBgDQoNCklmIGFueSB2YXJpYWJsZXMgaGF2ZSBuZWFyLXplcm8gdmFyaWFuY2UsIGNvbnNpZGVyIHJlbW92aW5nIHRoZW0uIFRoaXMgaXMgbm90IG91ciBwcm9ibGVtLg0KDQpfXzIuIENoZWNrIGZvciBDb2xsaW5lYXJpdHlfXw0KSGlnaGx5IGNvcnJlbGF0ZWQgcHJlZGljdG9ycyBjYW4gY2F1c2UgcmFuayBkZWZpY2llbmN5LiBDb21wdXRlIHRoZSBjb3JyZWxhdGlvbiBtYXRyaXg6DQoNCmBgYHtyfQ0KY29yKHRyYWluX2RhdGEgJT4lIHNlbGVjdF9pZihpcy5udW1lcmljKSwgdXNlID0gInBhaXJ3aXNlLmNvbXBsZXRlLm9icyIpDQpgYGANCllvdSBjYW4gcmVtb3ZlIGhpZ2hseSBjb3JyZWxhdGVkIGZlYXR1cmVzIHVzaW5nIHNvbWV0aGluZyBsaWtlIChOb3RlOiBJIG1pc3BlbGxlZCBgdHJhaW5fZGF0YWAgc28gdGhpcyB3b3VsZG4ndCB3b3JrKToNCg0KYGBge3IsZXZhbD1GQUxTRX0NCnRyYWluaW5nX2RhdGEgPC0gdHJhaW5pbmdfZGF0YSAlPiUNCiAgZHBseXI6OnNlbGVjdCgtb25lX29mKGZpbmRDb3JyZWxhdGlvbihjb3JfbWF0cml4LCBjdXRvZmYgPSAwLjkpKSkNCmBgYA0KQW5kIHRyeSByZXJ1bm5pbmcgdGhlIG1vZGVsIGFmdGVyIGRyb3BwaW5nIGNvbGxpbmVhciB2YXJpYWJsZXMuIE91ciBsYXJnZXN0IGNvcnJlbGF0aW9uIGlzIGJldHdlZW4gYGxhdGl0dWRlYCBhbmQgYGxvbmdpdHVkZWAgd2hpY2ggbWFrZXMgc2Vuc2UuIFRoaXMgaXMgYWxzbyBub3Qgb3VyIHByb2JsZW0uDQoNCl9fMy4gRW5zdXJlIEJhbGFuY2VkIENsYXNzIERpc3RyaWJ1dGlvbl9fICANClFEQSBjYW4gc3RydWdnbGUgaWYgY2xhc3NlcyBhcmUgaGlnaGx5IGltYmFsYW5jZWQuIENoZWNrIHRoZSBjbGFzcyBkaXN0cmlidXRpb246ICANCg0KYGBge3J9DQp0YWJsZSh0cmFpbl9kYXRhJHByaWNlX2NhdGVnb3J5KQ0KYGBgDQoNCklmIGltYmFsYW5jZSBleGlzdHMsIHRyeSB1c2luZyBTTU9URSwgZG93bnNhbXBsaW5nLCBvciB1cHNhbXBsaW5nIHdoaWNoIHdlIGRpZCBhYm92ZQ0KDQpgc3RlcF9zbW90ZShwcmljZV9jYXRlZ29yeSlgIA0KDQpfXzQuIENoZWNrIGZvciBNaXNzaW5nIFZhbHVlc19fDQpNaXNzaW5nIHZhbHVlcyBjYW4gYWxzbyBjYXVzZSBlcnJvcnMuIENoZWNrIGFuZCBoYW5kbGUgbWlzc2luZyBkYXRhOg0KDQpgYGB7cn0NCnN1bShpcy5uYSh0cmFpbmluZ19kYXRhKSkNCmBgYA0KDQpOb3QgdGhpcyBlaXRoZXIuDQoNCl9fNS4gVHJ5IGFuIEFsdGVybmF0aXZlIE1vZGVsX18NClNvbWV0aW1lcyBtb2RlbHMgY2FuJ3QgZml0IGEgZGF0YSBzZXQgZGVzcGl0ZSB5b3VyIGJlc3QgZWZmb3J0cy4gSWYgdGhlIHByb2JsZW0gcGVyc2lzdHMsIHRyeSBbYW5vdGhlciBtb2RlbF0oaHR0cHM6Ly93d3cudGlkeW1vZGVscy5vcmcvZmluZC9wYXJzbmlwLykuDQoNCg0KDQojIyMjIEstbmVhcmVzdCBuZWlnaGJvciAgDQpXZSBkb27igJl0IHJlcGVhdCBhbGwgb2YgdGhlIHN0ZXBzIHNob3duIGluIGxvZ2lzdGljIHJlZ3Jlc3Npb24gYW5kIGp1c3QgZm9jdXMgb24gdGhlIHBlcmZvcm1hbmNlIG1ldHJpY3MuIA0KDQpgYGB7cn0NCmtubl9yZXMgPC0gDQogIGtubl93ZmxvdyB8Pg0KICBmaXRfcmVzYW1wbGVzKA0KICAgIHJlc2FtcGxlcyA9IGN2X2ZvbGRzLCANCiAgICBtZXRyaWNzID0gbWV0cmljX3NldCgNCiAgICAgIHlhcmRzdGljazo6cmVjYWxsLCANCiAgICAgIHlhcmRzdGljazo6cHJlY2lzaW9uLCANCiAgICAgIHlhcmRzdGljazo6Zl9tZWFzLCANCiAgICAgIHlhcmRzdGljazo6YWNjdXJhY3ksIA0KICAgICAgeWFyZHN0aWNrOjprYXAsIA0KICAgICAgeWFyZHN0aWNrOjpyb2NfYXVjLCANCiAgICAgIHlhcmRzdGljazo6c2VucywgDQogICAgICB5YXJkc3RpY2s6OnNwZWMNCiAgICApLA0KICAgIGNvbnRyb2wgPSBjb250cm9sX3Jlc2FtcGxlcyhzYXZlX3ByZWQgPSBUUlVFKQ0KICAgICkgDQoNCmtubl9yZXMgfD4gY29sbGVjdF9tZXRyaWNzKHN1bW1hcml6ZSA9IFRSVUUpDQpgYGANCg0KDQojIyMjIENvbXBhcmUgbW9kZWxzDQpFeHRyYWN0IG1ldHJpY3MgZnJvbSBvdXIgbW9kZWxzIHRvIGNvbXBhcmUgdGhlbToNCg0KYGBge3J9DQpsb2dfbWV0cmljcyA8LSANCiAgbG9nX3JlcyB8Pg0KICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gVFJVRSkgJT4lDQogIG11dGF0ZShtb2RlbCA9ICJMb2dpc3RpYyBSZWdyZXNzaW9uIikgIyBhZGQgdGhlIG5hbWUgb2YgdGhlIG1vZGVsIHRvIGV2ZXJ5IHJvdw0KDQpsZGFfbWV0cmljcyA8LSANCiAgbGRhX3JlcyB8Pg0KICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXplID0gVFJVRSkgJT4lDQogIG11dGF0ZShtb2RlbCA9ICJMaW5lYXIgRGlzY3JpbWluYW50IikNCg0KIyBRREEgZGlkIG5vdCBydW4NCiMgcWRhX21ldHJpY3MgPC0gDQojIHFkYV9yZXMgfD4NCiMgICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXNlID0gVFJVRSkgJT4lDQojICAgbXV0YXRlKG1vZGVsID0gIlF1YWRyYXRpYyBEaXNjcmltaW5hbnQiKQ0KDQojIGtubl9tZXRyaWNzIDwtIA0KIyAgIGtubl9yZXMgfD4NCiMgICBjb2xsZWN0X21ldHJpY3Moc3VtbWFyaXNlID0gVFJVRSkgJT4lDQojICAgbXV0YXRlKG1vZGVsID0gIktOTiIpDQoNCiMgY3JlYXRlIGRhdGFmcmFtZSB3aXRoIGFsbCBtb2RlbHMNCm1vZGVsX2NvbXBhcmUgPC0gYmluZF9yb3dzKA0KICAgICAgICAgICAgICAgICAgICAgICAgICBsb2dfbWV0cmljcywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgbGRhX21ldHJpY3MNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICkgDQoNCiMgY2hhbmdlIGRhdGEgc3RydWN0dXJlDQptb2RlbF9jb21wIDwtIA0KICBtb2RlbF9jb21wYXJlIHw+DQogIGRwbHlyOjpzZWxlY3QobW9kZWwsIC5tZXRyaWMsIG1lYW4sIHN0ZF9lcnIpIHw+DQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSAubWV0cmljLCB2YWx1ZXNfZnJvbSA9IGMobWVhbiwgc3RkX2VycikpIA0KDQojIHNob3cgbWVhbiBGMS1TY29yZSBmb3IgZXZlcnkgbW9kZWwNCm1vZGVsX2NvbXAgfD4NCiAgYXJyYW5nZShtZWFuX2ZfbWVhcykgfD4NCiAgbXV0YXRlKG1vZGVsID0gZmN0X3Jlb3JkZXIobW9kZWwsIG1lYW5fZl9tZWFzKSkgfD4jIG9yZGVyIHJlc3VsdHMNCiAgZ2dwbG90KGFlcyhtb2RlbCwgbWVhbl9mX21lYXMsIGZpbGw9bW9kZWwpKSArDQogIGdlb21fY29sKCkgKw0KICBjb29yZF9mbGlwKCkgKw0KICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlID0gIkJsdWVzIikgKw0KICAgZ2VvbV90ZXh0KA0KICAgICBzaXplID0gMywNCiAgICAgYWVzKGxhYmVsID0gcm91bmQobWVhbl9mX21lYXMsIDIpLCB5ID0gbWVhbl9mX21lYXMgKyAwLjA4KSwNCiAgICAgdmp1c3QgPSAxDQogICkNCmBgYA0KDQpgYGB7cn0NCiMgc2hvdyBtZWFuIGFyZWEgdW5kZXIgdGhlIGN1cnZlIChhdWMpIHBlciBtb2RlbA0KbW9kZWxfY29tcCB8Pg0KICBhcnJhbmdlKG1lYW5fcm9jX2F1YykgfD4NCiAgbXV0YXRlKG1vZGVsID0gZmN0X3Jlb3JkZXIobW9kZWwsIG1lYW5fcm9jX2F1YykpICU+JQ0KICBnZ3Bsb3QoYWVzKG1vZGVsLCBtZWFuX3JvY19hdWMsIGZpbGw9bW9kZWwpKSArDQogIGdlb21fY29sKCkgKw0KICBjb29yZF9mbGlwKCkgKw0KICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlID0gIkJsdWVzIikgKyANCiAgICAgZ2VvbV90ZXh0KA0KICAgICBzaXplID0gMywNCiAgICAgYWVzKGxhYmVsID0gcm91bmQobWVhbl9yb2NfYXVjLCAyKSwgeSA9IG1lYW5fcm9jX2F1YyArIDAuMDgpLA0KICAgICB2anVzdCA9IDENCiAgKQ0KYGBgDQoNCk5vdGUgdGhhdCB0aGUgbW9kZWwgcmVzdWx0cyBhcmUgYWxsIHF1aXRlIHNpbWlsYXIuIEluIG91ciBleGFtcGxlIHdlIGNob29zZSB0aGUgRjEtU2NvcmUgYXMgcGVyZm9ybWFuY2UgbWVhc3VyZSB0byBzZWxlY3QgdGhlIGJlc3QgbW9kZWwuIExldOKAmXMgZmluZCB0aGUgbWF4aW11bSBtZWFuIEYxLVNjb3JlOg0KDQpgYGB7cn0NCm1vZGVsX2NvbXAgfD4gc2xpY2VfbWF4KG1lYW5fZl9tZWFzKQ0KYGBgDQoNCiMjIyBMYXN0IGV2YWx1YXRpb24gb24gdGVzdCBzZXQNClRpZHltb2RlbHMgcHJvdmlkZXMgdGhlIGZ1bmN0aW9uIGBsYXN0X2ZpdCgpYCB3aGljaCBmaXRzIGEgbW9kZWwgdG8gdGhlIHdob2xlIHRyYWluaW5nIGRhdGEgYW5kIGV2YWx1YXRlcyBpdCBvbiB0aGUgdGVzdCBzZXQuIFdlIGp1c3QgbmVlZCB0byBwcm92aWRlIHRoZSB3b3JrZmxvdyBvYmplY3Qgb2YgdGhlIGJlc3QgbW9kZWwgYXMgd2VsbCBhcyB0aGUgZGF0YSBzcGxpdCBvYmplY3QgKG5vdCB0aGUgdHJhaW5pbmcgZGF0YSkuDQoNCmBgYHtyfQ0KbGFzdF9maXRfbG9naXQgPC0gbGFzdF9maXQobG9nX3dmbG93LCANCiAgICAgICAgICAgICAgICAgICAgICAgIHNwbGl0ID0gZGF0YV9zcGxpdCwNCiAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpY3MgPSBtZXRyaWNfc2V0KA0KICAgICAgICAgICAgICAgICAgICAgICAgeWFyZHN0aWNrOjpyZWNhbGwsIA0KICAgICAgICAgICAgICAgICAgICAgICAgeWFyZHN0aWNrOjpwcmVjaXNpb24sIA0KICAgICAgICAgICAgICAgICAgICAgICAgeWFyZHN0aWNrOjpmX21lYXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgeWFyZHN0aWNrOjphY2N1cmFjeSwgDQogICAgICAgICAgICAgICAgICAgICAgICB5YXJkc3RpY2s6OmthcCwgDQogICAgICAgICAgICAgICAgICAgICAgICB5YXJkc3RpY2s6OnJvY19hdWMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgeWFyZHN0aWNrOjpzZW5zLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHlhcmRzdGljazo6c3BlYw0KICAgICAgICAgICAgICAgICAgICAgICAgKSkNCmBgYA0KDQpTaG93IHBlcmZvcm1hbmNlIG1ldHJpY3MNCg0KYGBge3J9DQpsYXN0X2ZpdF9sb2dpdCB8Pg0KICBjb2xsZWN0X21ldHJpY3MoKQ0KYGBgDQpBbmQgdGhlc2UgYXJlIG91ciBmaW5hbCBwZXJmb3JtYW5jZSBtZXRyaWNzLiBSZW1lbWJlciB0aGF0IGlmIGEgbW9kZWwgZml0IHRvIHRoZSB0cmFpbmluZyBkYXRhc2V0IGFsc28gZml0cyB0aGUgdGVzdCBkYXRhc2V0IHdlbGwsIG1pbmltYWwgX292ZXJmaXR0aW5nXyBoYXMgdGFrZW4gcGxhY2UuIFRoaXMgc2VlbXMgdG8gYmUgYWxzbyB0aGUgY2FzZSBpbiBvdXIgZXhhbXBsZS4NCg0KVG8gbGVhcm4gbW9yZSBhYm91dCB0aGUgbW9kZWwgd2UgY2FuIGFjY2VzcyB0aGUgdmFyaWFibGUgaW1wb3J0YW5jZSBzY29yZXMgdmlhIHRoZSBgLndvcmtmbG93YCBjb2x1bW4uIFdlIGZpcnN0IG5lZWQgdG8gcGx1Y2sgb3V0IHRoZSBmaXJzdCBlbGVtZW50IGluIHRoZSB3b3JrZmxvdyBjb2x1bW4sIHRoZW4gcHVsbCBvdXQgdGhlIGZpdCBmcm9tIHRoZSB3b3JrZmxvdyBvYmplY3QuIEZpbmFsbHksIHRoZSBgdmlwYCBwYWNrYWdlIGhlbHBzIHVzIHZpc3VhbGl6ZSB0aGUgdmFyaWFibGUgaW1wb3J0YW5jZSBzY29yZXMgZm9yIHRoZSB0b3AgZmVhdHVyZXMuIE5vdGUgdGhhdCB3ZSBjYW7igJl0IGNyZWF0ZSB0aGlzIHR5cGUgb2YgcGxvdCBmb3IgZXZlcnkgbW9kZWwgZW5naW5lLg0KDQpgYGB7cn0NCmxhc3RfZml0X2xvZ2l0IHw+DQogIHBsdWNrKCIud29ya2Zsb3ciLCAxKSB8PiAgDQogIGV4dHJhY3RfZml0X3BhcnNuaXAoKSB8Pg0KICB2aXAobnVtX2ZlYXR1cmVzID0gMTApDQpgYGANCg0KVGhlIHR3byBtb3N0IGltcG9ydGFudCBwcmVkaWN0b3JzIGluIHdoZXRoZXIgYSBkaXN0cmljdCBoYXMgYSBtZWRpYW4gaG91c2UgdmFsdWUgYWJvdmUgb3IgYmVsb3cgJDE1MCwwMDAgZG9sbGFycyB3ZXJlIHRoZSBvY2VhbiBwcm94aW1pdHkgaW5sYW5kIGFuZCB0aGUgbWVkaWFuIGluY29tZS4NCg0KVGFrZSBhIGxvb2sgYXQgdGhlIGNvbmZ1c2lvbiBtYXRyaXg6DQoNCmBgYHtyfQ0KbGFzdF9maXRfbG9naXQgJT4lDQogIGNvbGxlY3RfcHJlZGljdGlvbnMoKSB8Pg0KICBjb25mX21hdChwcmljZV9jYXRlZ29yeSwgLnByZWRfY2xhc3MpIHw+DQogIGF1dG9wbG90KHR5cGUgPSAiaGVhdG1hcCIpDQpgYGANCg0KTGV04oCZcyBjcmVhdGUgdGhlIFJPQyBjdXJ2ZS4gQWdhaW4sIHNpbmNlIHRoZSBldmVudCB3ZSBhcmUgcHJlZGljdGluZyBpcyB0aGUgZmlyc3QgbGV2ZWwgaW4gdGhlIHByaWNlX2NhdGVnb3J5IGZhY3RvciAo4oCcYWJvdmXigJ0pLCB3ZSBwcm92aWRlIGByb2NfY3VydmUoKWAgd2l0aCB0aGUgcmVsZXZhbnQgY2xhc3MgcHJvYmFiaWxpdHkgYC5wcmVkX2Fib3ZlYDoNCg0KYGBge3J9DQpsYXN0X2ZpdF9sb2dpdCB8Pg0KICBjb2xsZWN0X3ByZWRpY3Rpb25zKCkgfD4NCiAgcm9jX2N1cnZlKHByaWNlX2NhdGVnb3J5LCAucHJlZF9hYm92ZSkgfD4NCiAgYXV0b3Bsb3QoKQ0KYGBgDQoNCg0KQmFzZWQgb24gYWxsIG9mIHRoZSByZXN1bHRzLCB0aGUgdmFsaWRhdGlvbiBzZXQgYW5kIHRlc3Qgc2V0IHBlcmZvcm1hbmNlIHN0YXRpc3RpY3MgYXJlIHZlcnkgY2xvc2UsIHNvIHdlIHdvdWxkIGhhdmUgcHJldHR5IGhpZ2ggY29uZmlkZW5jZSB0aGF0IG91ciBsb2dpdCBtb2RlbCB3b3VsZCBwZXJmb3JtIHdlbGwgd2hlbiBwcmVkaWN0aW5nIG5ldyBkYXRhLg0K