knitr::opts_chunk$set(fig.width = 10, fig.height = 10, fig.path = 'Figs/', warning = FALSE, message = FALSE)

1 Introduction

Customer churn occurs when customers or subscribers stop doing business with a company or service. This is also called customer attrition or customer defection. Many service providers such as telephone companies, internet or insurance firms, banks often use customer churn analysis as one of the key business metrics because the cost of retaining an existing customer is far less than acquiring new ones.

1.1 About the data

  1. The target variable is called Churn - it represents customers who left within the last month.
  2. Demographic information of customers is captured in gender, age, partners and dependents variables.
  3. Customer account information is present in contract, payment method, Monthly charges, total charges and tenure.
  4. Services opted by customers are in Phone service, Multiple lines, Online backup, Tech support, Streaming Movies/TV.

2 Objective

This the demonstration in R to analyze all relevant customer data and compare various machine learning models including deep neural networks to predict customer churn.

3 Load libraries and functions

Load packages like tidyverse, purrr, gridExtra, caret and functions.

library("readr")
library("dplyr")
library("tidyr")
library("purrr")
library("corrplot")
library("stringr")
library("ROCR")
library("ggplot2")
library("gridExtra")
library("caret")
library("kableExtra")
library("keras")
library("doParallel")


# This function calculates the confusion matrix
cm <- function(model, data, target){
confusionMatrix(predict(model, newdata = data), target, mode = "prec_recall")
}

# This function identifies and plots the best probability cutoff values by maximizing tpr and fpr values from the ROC plot 
roc_cutoff <- function(model, data, target) {
# Check for the stack models  
if(str_detect(deparse(substitute(model)),'stack')) {
pred <- predict(model, data, type = "prob") %>% data.frame(No = ., Yes = 1 -.)
pred <- pred[,1]
}
else{
pred <- predict(model, data, type = 'prob')[,2]
}
# ROCR considers the later level to be the positive class. 
pred <- prediction(pred, target, label.ordering = c("No","Yes"))
eval <- performance(pred, "tpr","fpr")
# Calculate AUC value
pref_auc <- performance(pred, "auc")
auc <- pref_auc@y.values[[1]]

plot(eval)

# maximize the TPR and FPR
max <- which.max(slot(eval,"y.values")[[1]] +  1 - slot(eval,"x.values")[[1]])
# get the best cutoff value
cutoff <- slot(eval, "alpha.values")[[1]][max]
tpr <- slot(eval, "y.values")[[1]][max]
fpr <- slot(eval, "x.values")[[1]][max]
abline(h = tpr, v = fpr, lty = 2, col = "blue") # best cutoff
text(0.7,0.2, paste0("At best cutoff = ", round(cutoff,2)), col = "blue")
# Default cutoff
default <- last(which(slot(eval, "alpha.values")[[1]] >= 0.5))
defaulty <- slot(eval,"y.values")[[1]][default]
defaultx <- slot(eval,"x.values")[[1]][default]
abline(h = defaulty, v = defaultx, col = "red", lty = 2) # Default cutoff
text(0.7,0.3, paste0("At default cutoff = ", 0.50), col = "red")
text(0.7, 0.4, paste0("AUC = ", round(auc,2)))
return(cutoff)
}

# Find the missing values in the columns of the dataframe
missing_values <- function(df){
  
missing <- df %>% gather(key = "key", value = "value") %>% mutate(is.missing = is.na(value)) %>% 
  group_by(key) %>% mutate(total = n()) %>% 
  group_by(key, total, is.missing) %>%
    summarise(num.missing = n()) %>% 
    mutate(perc.missing = num.missing/total * 100) %>% ungroup()
  
  return(missing)
}

# Plot add on services
plot_addon_services <- function(df,group_var){
  # Filter customers who have internet at home
  withInternet <- df %>% filter(InternetService %ni% c("No"))
  # Group by Churn and add on service var and calculate the Percentage of customers
  withInternet %>% group_by(Churn, !! sym(group_var)) %>% tally(name = "count") %>% 
    mutate(Percent = count /nrow(withInternet)) %>%
    mutate(Percent = round(Percent,2)) %>%
    # Plot the percentage and Churn behaviour of each group
    ggplot(aes(x = !! sym(group_var), y = Percent, fill = Churn)) + geom_bar(stat = "identity") + 
    geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + 
    theme_minimal() + scale_fill_brewer(palette="Dark2") + 
    scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + 
    theme(axis.text.x = element_text(angle = 90, hjust = 1))
}

'%ni%' <- Negate('%in%')

4 Data preprocessing and cleaning

The data was downloaded from Kaggle. The data contains 7043 rows and 21 columns (variables) including the binary target variable called Churn.


churn <- read_csv("C:\\Users\\khannva1\\Documents\\DS-Projects\\telco-customer-churn\\WA_Fn-UseC_-Telco-Customer-Churn.csv")
print(churn)
  1. Replace “No internet service” to “No”.
  2. Remove “(automatic)” from PaymentMethod column.
  3. Recode SeniorCitizen column to factor. Make “1” = “Yes” and “0” = “No”
  4. Recode tenure column to categorical column.
  5. Remove customerID column from the dataset.
  6. Convert character columns to factor columns.
# Map "No internet service to No"

recode <- c("OnlineSecurity", "OnlineBackup", "DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies", "MultipleLines")

churn <- as.data.frame(churn)

for (col in recode) {
  churn[,col] <- as.character(churn[,col])
  temp <- if_else(churn[,col] %in% c("No internet service","No phone service"), "No",churn[,col])
churn[,col] <- as.factor(temp)
}

# Remove (automatic) from PaymentMethod
churn$PaymentMethod <- str_remove(churn$PaymentMethod, "\\(automatic\\)") %>% str_trim(., side = "right") %>% as.factor()

# Does not make sense to have senior citizen as numbers. 
churn$SeniorCitizen <- as.factor(recode(churn$SeniorCitizen, "1" = "Yes", "0" = "No"))

# Make tenure as categorical variable for easier ploting 
churn <- churn %>% mutate(tenure_group = case_when(tenure <= 12 ~ "0-12M",
                                           tenure >12 & tenure <=24 ~ "12-24M",
                                           tenure > 24 & tenure <= 48 ~ "24-48M",
                                           tenure > 48 & tenure <= 60 ~ "48-60M",
                                           tenure >60 ~ ">60M"
                ))
churn$tenure_group <- as.factor(churn$tenure_group)

# Remove columns not needed

churn <- churn %>% select(-one_of(c("customerID")))

# Turn character columns to factor
recode <- churn %>% select_if(is.character) %>% colnames()

for(col in recode){
  churn[,col] <-  as.factor(churn[,col])

}

## Look at the unique values after cleaning 

churn[-1] %>% select_if(is.factor) %>% map(function(x) unique(x))
$SeniorCitizen
[1] No  Yes
Levels: No Yes

$Partner
[1] Yes No 
Levels: No Yes

$Dependents
[1] No  Yes
Levels: No Yes

$PhoneService
[1] No  Yes
Levels: No Yes

$MultipleLines
[1] No  Yes
Levels: No Yes

$InternetService
[1] DSL         Fiber optic No         
Levels: DSL Fiber optic No

$OnlineSecurity
[1] No  Yes
Levels: No Yes

$OnlineBackup
[1] Yes No 
Levels: No Yes

$DeviceProtection
[1] No  Yes
Levels: No Yes

$TechSupport
[1] No  Yes
Levels: No Yes

$StreamingTV
[1] No  Yes
Levels: No Yes

$StreamingMovies
[1] No  Yes
Levels: No Yes

$Contract
[1] Month-to-month One year       Two year      
Levels: Month-to-month One year Two year

$PaperlessBilling
[1] Yes No 
Levels: No Yes

$PaymentMethod
[1] Electronic check Mailed check     Bank transfer    Credit card     
Levels: Bank transfer Credit card Electronic check Mailed check

$Churn
[1] No  Yes
Levels: No Yes

$tenure_group
[1] 0-12M  24-48M 12-24M >60M   48-60M
Levels: >60M 0-12M 12-24M 24-48M 48-60M

4.1 Missing values

missing <- missing_values(churn)

p1 <- ggplot(missing) + geom_bar(aes( x= reorder(key, desc(perc.missing)), y = perc.missing, fill = is.missing), stat = 'identity', alpha = 0.8) + scale_fill_manual(name = "", values = c('steelblue','tomato3'), label = c("Present", "Missing")) + coord_flip() + labs(title = "Percentage of missing values",x = '',y = '% missing')

# Plot missing values
p1


# remove rows with missing values
churn <- na.omit(churn)

# One hot encoding factor variables

# factors_var <- churn %>% select( -c("Churn","TotalCharges", "MonthlyCharges")) %>% names()
# formula <- paste(factors_var, "+ ", collapse = "")
# 
# dmy <- dummyVars("~gender + SeniorCitizen + Partner + Dependents + PhoneService + MultipleLines + InternetService + OnlineSecurity + OnlineBackup + DeviceProtection + TechSupport + StreamingTV + StreamingMovies + Contract + PaperlessBilling + PaymentMethod + tenure_group", data = churn)
# churn_data <- predict(dmy, newdata = churn)

5 Exploratory data analysis

print("Percentage of customers churn")
[1] "Percentage of customers churn"
prop.table(table(churn$Churn))

      No      Yes 
0.734215 0.265785 

From the table above we notice that 73.4% of the customers did not churn. This can serve as our baseline model i.e. if we predict every customer to not churn we will be right on average 73.4% of the time. Let us explore the data further.

5.1 Correlation plot between numeric variables

# Correlation matrix of numeric variables
cor_matrix<- churn %>% select_if(is.numeric) %>% cor()

corrplot(cor_matrix,method = "number", type = "upper")

5.2 Plots of categorical variables


p1 <- churn %>% group_by(gender, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = gender, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p2 <- churn %>% group_by(SeniorCitizen, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = SeniorCitizen, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.0, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p3 <- churn %>% group_by(Partner, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = Partner, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p4 <- churn %>% group_by(Dependents, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = Dependents, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p5 <- churn %>% group_by(PhoneService, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = PhoneService, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.0, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p6 <- churn %>% group_by(MultipleLines, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = MultipleLines, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p7 <- churn %>% group_by(InternetService, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = InternetService, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p8 <- churn %>% group_by(OnlineSecurity, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = OnlineSecurity, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

grid.arrange(p1, p2, p3, p4, p5, p6, p7, p8, ncol=4)

  1. Does gender makes a difference? It does not seem to play any significant role in predicting customer churn.

  2. How does Senior Citizen behave? Almost 50% of the total Senior citizen’s population churn. A company could come up with offers specifically catering for senior citizen demands.

  3. Does having a partner makes a difference? It seems like a customer without a partner is more likely to churn than customers with the partner.

  4. Effect of dependents? Customers with dependents are more likely to stay than without. However, customers with dependents are only 30% of the total customer population.

  5. Phone service Customers without phone service are less likely to churn however, they form only 9% of the total customers.

  6. Multiple lines I don’t know what multiple lines mean. However, it seems that customers with multiple lines are more likely to churn.

  7. Customers with Internet services It is very interesting to see that customers with Fiber optic internet service at home are highly prone to churning.

  8. Customers with Online security Customers with online security are more likely to stay.

p1 <- churn %>% group_by(OnlineBackup, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = OnlineBackup, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))


p2 <- churn %>% group_by(DeviceProtection, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = DeviceProtection, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p3 <- churn %>% group_by(TechSupport, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = TechSupport, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p4 <- churn %>% group_by(StreamingTV, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = StreamingTV, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p5 <- churn %>% group_by(StreamingMovies, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = StreamingMovies, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p6 <- churn %>% group_by(Contract, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = Contract, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + theme(axis.text.x = element_text(angle = 90, hjust = 1))

p7 <- churn %>% group_by(PaperlessBilling, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = PaperlessBilling, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1))

p8 <- churn %>% group_by(PaymentMethod, Churn) %>% summarise(Percent = round(n()/nrow(.),2)) %>% ggplot(aes(x = PaymentMethod, y = Percent, fill = Churn)) + geom_bar(stat = "identity") + geom_text(aes(label = Percent * 100), vjust = 1.5, hjust = 0.5,color = "white", size = 5.0) + theme_minimal() + scale_fill_brewer(palette="Dark2") + scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + theme(axis.text.x = element_text(angle = 90, hjust = 1))

grid.arrange(p1, p2, p3, p4, p5, p6, p7, p8, ncol=4)

What is the effect of internet addons on customer churning? Let us explore.

  1. Online backup and Device Protection service: Customers with online backup and device protection services are less likely to churn. They like these add ons.

  2. Technical support Customers who opt for technical support are less likely to churn. Although fewer customers opt for technical support service however if they do they seem to stay the company.

  3. Does Streaming services help retain customers? I would thik that those customers who opt for streaming services are more likely to stay interestingly, it is the opposite they are more prone to churning.

  4. What about Contract period? Very interesting pattern. If the company can engage customers in 1 year or more contract customers are almost certain to stay.

  5. Paperless billing. It is interesting to see that customers who opt for paperless billing service are more likely to churn. It may be because people tend to forget paying bills and if the bill is hiding somewhere in their email or SMS it becomes even more difficult to remember and pay on time, therefore, most likely they miss the due date and end up paying extra which makes them unhappy with the company and they churn.

  6. Payment method Very interesting pattern. Of all the customers paying bills through electronic check almost half of the customers churn. It may be due to the bad experience like bouncing of checks or some error on the company’s part in processing check payments.

5.3 Some other interesting plots

Let us take a deeper look at the popularity of add on services such as Tech Support, Streaming Movies, Online backup, etc. for the customers who have the internet at home. For this, we will filter out customers with the internet at home.

# Create an adds on vector
adds_on <- names(churn)[7:13]

addson_plots <- map(adds_on, plot_addon_services, df = churn)

p1 <- addson_plots[[1]] + ggtitle("Popularity of add on services")
p2 <- addson_plots[[2]]
p3 <- addson_plots[[3]]
p4 <- addson_plots[[4]]
p5 <- addson_plots[[5]]
p6 <- addson_plots[[6]]
p7 <- addson_plots[[7]]

grid.arrange(p1, p2, p3, p4, p5, p6, p7, ncol=4)

  1. Internet service: Customers with Fiber optic service are more prone to churning than DSL service. It may be because DSL service may not be fast enough and customers use telco’s internet and therefore, are less likely to churn.

  2. Online security & Online backup: Customers who do not have Online security or backup service are more likely to churn.

  3. Device Protection and Tech Support: Similarly customers who do not opt for device protection and tech support are more likely to churn.

  4. Streaming services: Streaming services do not significantly make a difference in customer churn behaviour.

Additionally, we also see that popularity of services with customers is in the order StreamingMovies (49.5%) > StreamingTV (49.0%) > OnlineBackup (44.0%) > Device Protection (43.9%) > Tech support (37.0%) > Online security (36.6%).

5.4 Monthly charges paid by customers

Let us look at how much on average monthly charges are paid by customers who churn. My guess is customers who churn are paying higher monthly charges than customers who do not churn.

df <- churn %>% group_by(gender, tenure_group, Churn) %>% 
   summarise(mean = median(MonthlyCharges)) %>% arrange(tenure_group)
# Define the x positions for plotting text
df$x <- rep(c(0.8,1.2,1.8,2.2),5)

churn %>% ggplot(aes(y = MonthlyCharges,  x= gender, fill = Churn)) + 
    geom_boxplot() + facet_wrap(~tenure_group) + 
  geom_text(data = df, aes(label = round(mean, 1), y = mean + 3.0, x = x), size = 4) + 
  theme_minimal() + scale_fill_brewer(palette="Dark2")

  1. Customers who churn pay much higher monthly bills on average which makes sense since higher monthly bills would be hard on their pocket. It also seems to be a very distinguishing feature of people who churn.

  2. Another interesting pattern which we can see from the plot above is that as months pass by customers who do not churn are happy to pay more. For example the median monthly bills rise in the order 45.5,55.5,61.5,75.8,84.6 for female customers for tenures 0-12M, 12-24M, 24-48M, 48-60M and >60M, respectively.

churn$Internet <- ifelse(churn$InternetService %in% c("No"), "No", "Yes")

churn %>% ggplot(aes(x = MonthlyCharges, fill = Internet)) + 
    facet_wrap(~PaymentMethod) + theme_minimal() + 
    scale_fill_brewer(palette="Dark2") + 
    ggtitle(label = "Monthly Charges paid by customers through various payment methods with or without internet") +
    ylab(label = "Count") + geom_histogram()

From the plot above we can see that majority of customers who do not have internet service at home prefer to pay bills by Mailed check (which is expected). Additionally, such customers usually have a small amount to be paid.

Also, we can note that most common payment method of customers who have internet service at home is Electronic check although, Bank transfer and Credit card are also popular means of payment.

5.5 Senior Citizen customers


  churn %>% ggplot(aes(x = MonthlyCharges, fill = Internet)) + 
    geom_histogram() + facet_grid(SeniorCitizen~PaymentMethod) + theme_minimal() + 
    scale_fill_brewer(palette="Dark2") + 
    ggtitle(label = "Distribution of Monthly Charges paid by Senior Citizen Customers") +
    ylab(label = "Count")

It looks like overall Mailed checks again are the most common type of payment method for paying small monthly bills by customers. It is interesting to note that Senior Citizens do not like to mail checks rather prefer to pay by electronic check which is quite interesting to know because one would think that senior citizens would be more old school.

5.6 Monthly charges by tenure group

churn %>% group_by(Churn) %>% ggplot(aes(x = MonthlyCharges, fill = Churn)) + geom_histogram() + facet_wrap(~tenure_group) + theme_minimal() + scale_fill_brewer(palette="Dark2") + ggtitle(label = "Monthly Charges paid by Customers over the tenure ") + ylab(label = "Count")

From the plot above it looks like customers mostly churn when the tenure period is less than 12 months and Monthly charges are above 70 dollars.

6 Machine learning models

Split the data into the train (80%) and test set (20%). Also, define the parameters necessary for model tuning and cross-validation. Before I jump to machine learning I would like to standardize the numerical variables and remove highly correlated variables. I will remove redundant Internet variables that I created during exploratory data analysis for better visualization and log transform TotalCharges and normalize tenure variables.

# Create new variables
churn <- churn %>% mutate(
                          monthlyChargeOver69 = ifelse(MonthlyCharges > 69,"Yes","No") %>% as.factor(),
                          tenureLessThan29 = ifelse(tenure < 29, "Yes","No") %>% as.factor(),
                          TotalCharges = log(TotalCharges))


# Remove redundant variables
churn <- churn %>% select(-one_of(c("Internet")))

# Normalize the data 
pre_process <- churn %>% select_if(is.double) %>% preProcess(., method = c("center", "scale"))
churn <- predict(pre_process, newdata = churn)

# split the data
set.seed(3000)
inTrain <- createDataPartition(churn$Churn, p = 0.8, list = FALSE)

training<- churn[inTrain,]
testing<- churn[-inTrain,]

# Print the dimensions of train and test set
dimensions <- data.frame(matrix(c(dim(training), dim(testing)), ncol = 2, byrow = TRUE))
colnames(dimensions) <- c("Rows", "Columns")
rownames(dimensions) <- c("Train", "Test")

dimensions %>% kable() %>% 
  kable_styling(bootstrap_options = c("condensed","responsive"), full_width = F, position = "left", font_size = 10)

Rows Columns
Train 5627 23
Test 1405 23


train_y <- training %>% pull("Churn")
train_x <- training %>% select(-c("Churn"))

test_y <- testing %>% pull("Churn")
test_x <- testing %>% select(-c("Churn"))

# the logistic regression model parameters

metric <- "logLoss"

ctrl <- trainControl(
  method = "cv", 
  number = 5, 
  savePredictions = "all", 
  classProbs = TRUE, 
  summaryFunction = multiClassSummary, 
  verboseIter = FALSE)

6.1 Logistic Regression

set.seed(3000)

weights <- c(0.681,1.881)

model_weights <- ifelse(train_y == "No", weights[1], weights[2])

model_glm <- train(x = train_x, y = train_y, method = "glm", trControl = ctrl, metric = metric, weights = model_weights)

# Evaluate the model on the test data
cm_glm <- cm(model = model_glm, data = test_x, target = test_y)

print(cm_glm)
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  756  74
       Yes 276 299
                                          
               Accuracy : 0.7509          
                 95% CI : (0.7274, 0.7733)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.08636         
                                          
                  Kappa : 0.4554          
                                          
 Mcnemar's Test P-Value : < 2e-16         
                                          
              Precision : 0.9108          
                 Recall : 0.7326          
                     F1 : 0.8120          
             Prevalence : 0.7345          
         Detection Rate : 0.5381          
   Detection Prevalence : 0.5907          
      Balanced Accuracy : 0.7671          
                                          
       'Positive' Class : No              
                                          

The overall accuracy of the model is ~75.1% with the F1 score close to ~81.2%. Balanced Accuracy is 76.7%. Let us see if we can tweak the probability cutoff to achieve better accuracy.

# Get the probabilitis of prediction
p1 <- predict(model_glm, test_x, type = "prob")

cutoff <- roc_cutoff(model_glm, data = test_x, test_y)


opt_pred_glm <- ifelse(p1[,2] >= cutoff, "Yes","No")

# Optimized predictions
confusionMatrix(factor(opt_pred_glm), test_y, mode = "prec_recall")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  775  76
       Yes 257 297
                                         
               Accuracy : 0.763          
                 95% CI : (0.7399, 0.785)
    No Information Rate : 0.7345         
    P-Value [Acc > NIR] : 0.007976       
                                         
                  Kappa : 0.4738         
                                         
 Mcnemar's Test P-Value : < 2.2e-16      
                                         
              Precision : 0.9107         
                 Recall : 0.7510         
                     F1 : 0.8232         
             Prevalence : 0.7345         
         Detection Rate : 0.5516         
   Detection Prevalence : 0.6057         
      Balanced Accuracy : 0.7736         
                                         
       'Positive' Class : No             
                                         

By tweaking the cutoff we increased the F1 score of our model from ~81.2% to ~82.3% . Overall balanced accuracy metric also improved from ~76.7% to ~77.4% .

6.1.1 Feature analysis

# Feature analysis
print(summary(model_glm))

Call:
NULL

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-2.0957  -0.8389  -0.3782   0.4983   3.9030  

Coefficients:
                               Estimate Std. Error z value Pr(>|z|)    
(Intercept)                   -2.743558   1.366003  -2.008 0.044595 *  
genderMale                    -0.016349   0.068264  -0.239 0.810724    
SeniorCitizenYes               0.229415   0.091779   2.500 0.012431 *  
PartnerYes                    -0.006538   0.082113  -0.080 0.936540    
DependentsYes                 -0.046863   0.092049  -0.509 0.610676    
tenure                         0.034755   0.281596   0.123 0.901773    
PhoneServiceYes                0.714722   0.675945   1.057 0.290344    
MultipleLinesYes               0.639022   0.185911   3.437 0.000588 ***
InternetServiceFiber optic     2.497956   0.837303   2.983 0.002851 ** 
InternetServiceNo             -2.627517   0.842726  -3.118 0.001822 ** 
OnlineSecurityYes              0.022742   0.184477   0.123 0.901887    
OnlineBackupYes                0.138339   0.185124   0.747 0.454897    
DeviceProtectionYes            0.250812   0.183074   1.370 0.170685    
TechSupportYes                 0.038298   0.186696   0.205 0.837465    
StreamingTVYes                 0.825690   0.339632   2.431 0.015052 *  
StreamingMoviesYes             0.879489   0.340866   2.580 0.009875 ** 
ContractOne year              -0.701537   0.106847  -6.566 5.17e-11 ***
ContractTwo year              -1.608276   0.169501  -9.488  < 2e-16 ***
PaperlessBillingYes            0.268629   0.077460   3.468 0.000524 ***
PaymentMethodCredit card      -0.143644   0.113109  -1.270 0.204099    
PaymentMethodElectronic check  0.273708   0.096994   2.822 0.004774 ** 
PaymentMethodMailed check     -0.058988   0.117351  -0.503 0.615200    
MonthlyCharges                -1.342979   0.992184  -1.354 0.175877    
TotalCharges                  -0.892845   0.124649  -7.163 7.90e-13 ***
tenure_group0-12M              0.703992   0.510791   1.378 0.168129    
tenure_group12-24M             0.599098   0.438732   1.366 0.172089    
tenure_group24-48M             0.403618   0.330441   1.221 0.221915    
tenure_group48-60M             0.278207   0.204957   1.357 0.174656    
monthlyChargeOver69Yes        -0.326902   0.188276  -1.736 0.082513 .  
tenureLessThan29Yes           -0.232561   0.211177  -1.101 0.270785    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 7800.9  on 5626  degrees of freedom
Residual deviance: 5286.9  on 5597  degrees of freedom
AIC: 6781.6

Number of Fisher Scoring iterations: 5

The most important features according to glm model are are SeniorCitizen, MultipleLines,InternetService, Streaming services,Contract, PaperlessBilling, PaymentMethod and TotalCharges.

6.1.2 Let us remove less important variables

# Copy churn dataframe to churn2
churn2 <- churn

# Create a new column for add on services combined into one
adds_on_columns <- churn2 %>% select(c(adds_on[3:6])) %>% mutate_at(.vars = vars(OnlineSecurity:TechSupport), .funs = funs(ifelse(. == "No", 0, 1)))

churn2$adds_on <- rowSums(adds_on_columns) 

# Remove redundant variables and less important variables
churn2 <- churn2 %>% select(-one_of(c("gender", "Partner", "Dependents",
                                     "tenure","PhoneService" ,"OnlineSecurity",
                                     "OnlineBackup", "DeviceProtection","TechSupport", "MonthlyCharges",
                                     "changeInBillSign", "changeInBill","tenureLessThan29")))

# Normalize the data 
pre_process <- churn2 %>% select(adds_on) %>% preProcess(., method = c("center", "scale"))
churn2 <- predict(pre_process, newdata = churn2)

# rebuild the train and test set with the same split as before

training2<- churn2[inTrain,]
testing2<- churn2[-inTrain,]

# Print the dimensions of train and test set
dimensions <- data.frame(matrix(c(dim(training2), dim(testing2)), ncol = 2, byrow = TRUE))
colnames(dimensions) <- c("Rows", "Columns")
rownames(dimensions) <- c("Train", "Test")

dimensions %>% kable() %>% 
  kable_styling(bootstrap_options = c("condensed","responsive"), full_width = F, position = "left", font_size = 10)

Rows Columns
Train 5627 13
Test 1405 13


train_y2 <- training2 %>% pull("Churn")
train_x2 <- training2 %>% select(-c("Churn"))

test_y2 <- testing2 %>% pull("Churn")
test_x2 <- testing2 %>% select(-c("Churn"))

# train the new model
set.seed(3000)

weights <- c(0.681,1.881)

model_weights <- ifelse(train_y2 == "No", weights[1], weights[2])

model_glm <- train(x = train_x2, y = train_y2, method = "glm", trControl = ctrl, metric = metric, weights = model_weights)

# Evaluate the model on the test data
cm_glm <- cm(model = model_glm, data = test_x2, target = test_y2)

print(cm_glm)
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  753  76
       Yes 279 297
                                          
               Accuracy : 0.7473          
                 95% CI : (0.7238, 0.7699)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.1451          
                                          
                  Kappa : 0.448           
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
              Precision : 0.9083          
                 Recall : 0.7297          
                     F1 : 0.8092          
             Prevalence : 0.7345          
         Detection Rate : 0.5359          
   Detection Prevalence : 0.5900          
      Balanced Accuracy : 0.7629          
                                          
       'Positive' Class : No              
                                          

By removing the variables we slightly decreased the predictive ability of our logistic regression model. Balanced Accuracy decreased from ~76.7% to 76.3%. However, it is a much simpler model and thus would be less prone to overfitting. Let us see if we can improve the model by tweaking the cutoff.

# Get the probabilitis of prediction
p1 <- predict(model_glm, test_x2, type = "prob")

cutoff <- roc_cutoff(model_glm, data = test_x2, test_y2)


opt_pred_glm <- ifelse(p1[,2] >= cutoff, "Yes","No")

# Optimized predictions
confusionMatrix(factor(opt_pred_glm), test_y2, mode = "prec_recall")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  785  82
       Yes 247 291
                                          
               Accuracy : 0.7658          
                 95% CI : (0.7428, 0.7878)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.003931        
                                          
                  Kappa : 0.4739          
                                          
 Mcnemar's Test P-Value : < 2.2e-16       
                                          
              Precision : 0.9054          
                 Recall : 0.7607          
                     F1 : 0.8268          
             Prevalence : 0.7345          
         Detection Rate : 0.5587          
   Detection Prevalence : 0.6171          
      Balanced Accuracy : 0.7704          
                                          
       'Positive' Class : No              
                                          

Again by tweaking the cutoff we were able to increase the Balanced Accuracy of our model from ~76.3% to ~77.0% . However, it is slightly less than the previous more complex logistic regression model where the Balanced Accuracy was ~77.4% . Nevertheless, this is a better model because we used fewer variables and the model accuracy did not decrease significantly.

6.2 Decision trees

# Let us build decision tree model 
set.seed(3000)

model_cart <- train(x = train_x2, y = train_y2, method = "rpart", trControl = ctrl, metric = metric, tuneLength = 15, weights = model_weights)

cm_cart <- cm(model_cart,test_x2, test_y2)

print(cm_cart)
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  765  86
       Yes 267 287
                                          
               Accuracy : 0.7488          
                 95% CI : (0.7252, 0.7713)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.119           
                                          
                  Kappa : 0.4422          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
              Precision : 0.8989          
                 Recall : 0.7413          
                     F1 : 0.8125          
             Prevalence : 0.7345          
         Detection Rate : 0.5445          
   Detection Prevalence : 0.6057          
      Balanced Accuracy : 0.7554          
                                          
       'Positive' Class : No              
                                          

The Balanced Accuracy of the decision tree model is ~75.5% less than the logistic regression. Let us optimize it.

# Get the probabilitis of prediction
p2 <- predict(model_cart, test_x2, type = "prob")

cutoff <- roc_cutoff(model_cart, data = test_x2, test_y2)


opt_pred_cart <- ifelse(p1[,2] >= cutoff, "Yes","No")

# Optimized predictions
confusionMatrix(factor(opt_pred_cart), test_y2, mode = "prec_recall")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  847 122
       Yes 185 251
                                         
               Accuracy : 0.7815         
                 95% CI : (0.759, 0.8029)
    No Information Rate : 0.7345         
    P-Value [Acc > NIR] : 2.743e-05      
                                         
                  Kappa : 0.4684         
                                         
 Mcnemar's Test P-Value : 0.0004024      
                                         
              Precision : 0.8741         
                 Recall : 0.8207         
                     F1 : 0.8466         
             Prevalence : 0.7345         
         Detection Rate : 0.6028         
   Detection Prevalence : 0.6897         
      Balanced Accuracy : 0.7468         
                                         
       'Positive' Class : No             
                                         

Unlike the logistic regression, we were not able to improve the Balanced Accuracy of the DT model by optimizing it however, Recall and the F1 score improved dramatically.

6.3 Random forest

set.seed(3000)
model_rf <- train(x = train_x2, y = train_y2, method = "ranger", trControl = ctrl, metric = metric, tuneLength = 11, weights = model_weights)

cm_rf <- cm(model_rf,test_x2, test_y2)

print(cm_rf)
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  769  96
       Yes 263 277
                                          
               Accuracy : 0.7445          
                 95% CI : (0.7208, 0.7671)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.2078          
                                          
                  Kappa : 0.4268          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
              Precision : 0.8890          
                 Recall : 0.7452          
                     F1 : 0.8108          
             Prevalence : 0.7345          
         Detection Rate : 0.5473          
   Detection Prevalence : 0.6157          
      Balanced Accuracy : 0.7439          
                                          
       'Positive' Class : No              
                                          

The balanced accuracy of the random forest tree model is ~74.4 less than the logistic regression and decision tree model. Let us optimize it.

# Get the probabilitis of prediction
p3 <- predict(model_rf, test_x2, type = "prob")

cutoff <- roc_cutoff(model_rf, data = test_x2, test_y2)


opt_pred_rf <- ifelse(p1[,2] >= cutoff, "Yes","No")

# Optimized predictions
confusionMatrix(factor(opt_pred_rf), test_y2, mode = "prec_recall")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  695  58
       Yes 337 315
                                          
               Accuracy : 0.7189          
                 95% CI : (0.6946, 0.7422)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.9124          
                                          
                  Kappa : 0.4181          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
              Precision : 0.9230          
                 Recall : 0.6734          
                     F1 : 0.7787          
             Prevalence : 0.7345          
         Detection Rate : 0.4947          
   Detection Prevalence : 0.5359          
      Balanced Accuracy : 0.7590          
                                          
       'Positive' Class : No              
                                          

Like LR and DT models the balanced accuracy of the RF model was improved from ~74.4% to ~75.9% however, Recall and F1 score decreased significantly. The Specificity of the model is very high ~84.5%, in fact, the highest we have got till now. This means that the RF model is doing well in predicting customers who will churn however, there are many False Negatives as well.

6.4 Multilayer perceptron model

# Create one hot encoded variables
dmy <- dummyVars("~SeniorCitizen + MultipleLines + InternetService + StreamingTV + StreamingMovies + Contract + PaperlessBilling + PaymentMethod + tenure_group + monthlyChargeOver69", data = churn2)

ohe_churn <- predict(dmy, newdata = churn2) %>% as.data.frame()
num_churn <- churn2 %>% select_if(is.numeric)

preProcessObject <- preProcess(ohe_churn, method = c("center","scale"))
ohe_churn <- predict(preProcessObject, ohe_churn)

churn_data <- cbind(ohe_churn, num_churn) %>% as.matrix()

churn_data2 <- churn_data[,c("SeniorCitizen.Yes", "MultipleLines.Yes", "InternetService.DSL","InternetService.Fiber optic", "InternetService.No", "StreamingTV.Yes", "StreamingMovies.Yes","Contract.Month-to-month","Contract.One year","Contract.Two year", "PaperlessBilling.Yes", "PaymentMethod.Bank transfer", "PaymentMethod.Credit card", "PaymentMethod.Electronic check", "PaymentMethod.Mailed check", "tenure_group.>60M", "tenure_group.0-12M","tenure_group.12-24M","tenure_group.24-48M","tenure_group.48-60M", "monthlyChargeOver69.Yes", "TotalCharges", "adds_on")]

train_data <- churn_data2[inTrain,]
test_data <- churn_data2[-inTrain,]

train_labels <- to_categorical(as.integer(train_y2)-1, num_classes = 2)
test_labels <- to_categorical(as.integer(test_y2)-1, num_classes = 2)

Let us do the grid search with differnet optimizers, activators and number of hidden layers and choose the best one to build our final model.

Best model uses rmsprop optimizer with relu activator and hidden layer structure (8,4,2).

set.seed(3000)
class_weights = list('0' = weights[1], '1' = weights[2])

# Create a function to train the network
train_network <- function(structure, activation,optimizer, epochs){
  suppressMessages(use_session_with_seed(3000))
  model <- keras_model_sequential()
  
  model %>%
    layer_dense(units = structure[2], activation = activation, input_shape = structure[1]) %>%
    layer_dropout(rate = 0.3) %>%
    layer_dense(units = structure[3], activation = activation) %>%
    layer_dropout(rate = 0.2) %>%
    layer_dense(units = structure[4], activation = activation) %>%
    layer_dropout(rate = 0.1) %>%
    layer_dense(units = 2, activation = "softmax")
  
  model %>% 
    compile(loss = "binary_crossentropy", optimizer = optimizer, metric = c("accuracy"))

  history <- model %>%
    fit(x = train_data, y = train_labels, shuffle = T, epochs = epochs, batch_size = 600, 
        validation_split = 0.3, class_weight = class_weights, verbose = 0)

  history_df <- as.data.frame(history)
  acc <<- history_df[nrow(history_df),2]

  pref <- model %>% evaluate(test_data, test_labels, verbose = 0)

  testacc <<- pref$acc
  
  return(model)
}

sample_epochs <- 100

# Initialise empty lists to store results
train_acc <- c()
test_acc <- c()
combination_vector <- c()


# Create a vector listing all the activation functions we wish to test
activation_functions <- c("elu", "hard_sigmoid", "linear", "relu", "selu", "sigmoid",
                         "softplus", "softsign", "tanh")


# Create a vector listing all the optimization functions we wish to test
optimizer_functions <- c("adadelta", "adagrad", "adam", "adamax",
                        "nadam", "rmsprop", "sgd")

# Tune the network with different parameters

for(i in 1:length(optimizer_functions)){
    for(j in 1:length(activation_functions)){
      # Optimize hidden layers
      for(k in 2:16)
      {
        NN_structure <- c(ncol(train_data),k,as.integer(k/2),as.integer(k/4))
        combination <- paste("Optimizer:",optimizer_functions[i], "Activator:",
                             activation_functions[j], "Layers:",k, sep = " ")
        combination_vector <- append(combination_vector, combination)
        #print(combination)

        # Call the function
          model <- train_network(NN_structure, activation_functions[j], optimizer_functions[i], sample_epochs)
          train_acc <- append(train_acc, acc)
          test_acc <- append(test_acc, testacc)
      }
    }
  }


# Collect the results
combination_matrix <- str_split(combination_vector, pattern = " ", simplify = TRUE)

train_acc <- train_acc[!is.na(train_acc)]
test_acc <- test_acc[!is.na(test_acc)]

results <- data.frame(acc_train = train_acc, acc_test = test_acc, optimizer = combination_matrix[,2], activator = combination_matrix[,4], nLayers = combination_matrix[,6] ,stringsAsFactors = FALSE)

results %>% group_by(optimizer, activator) %>% top_n(., 1, acc_train) %>% ggplot(aes(x = acc_train, y = acc_train, color = optimizer)) + geom_point(size = 3.5) + facet_wrap(~activator)



# Final train using the best parameters
bst_para <- combination_vector[which.max(results$acc_test)]

bst_para <- str_split(bst_para, pattern = " ") %>% unlist()

bst_optimizer <- bst_para[2]
bst_activator <- bst_para[4]
bst_layer <- as.integer(bst_para[6])

bst_structure <- c(ncol(train_data),bst_layer,as.integer(bst_layer/2),as.integer(bst_layer/4))

model <- train_network(bst_structure, bst_activator, bst_optimizer, sample_epochs)

# get the summary of the model
summary(model) %>% kable() 
Model: "sequential"
______________________________________________________________________________________________________________________
Layer (type)                                         Output Shape                                   Param #           
======================================================================================================================
dense (Dense)                                        (None, 8)                                      192               
______________________________________________________________________________________________________________________
dropout (Dropout)                                    (None, 8)                                      0                 
______________________________________________________________________________________________________________________
dense_1 (Dense)                                      (None, 4)                                      36                
______________________________________________________________________________________________________________________
dropout_1 (Dropout)                                  (None, 4)                                      0                 
______________________________________________________________________________________________________________________
dense_2 (Dense)                                      (None, 2)                                      10                
______________________________________________________________________________________________________________________
dropout_2 (Dropout)                                  (None, 2)                                      0                 
______________________________________________________________________________________________________________________
dense_3 (Dense)                                      (None, 2)                                      6                 
======================================================================================================================
Total params: 244
Trainable params: 244
Non-trainable params: 0
______________________________________________________________________________________________________________________

# Evaluate the model on test set
model %>% evaluate(test_data, test_labels)

  32/1405 [..............................] - ETA: 0s - loss: 0.4744 - acc: 0.8125
1405/1405 [==============================] - 0s 25us/sample - loss: 0.5262 - acc: 0.7865
$loss
[1] 0.5262008

$acc
[1] 0.7864769
# Predict the classes and make a confusion matrix

 pred_nn <- model %>% predict_classes(test_data)
 pred_nn <- pred_nn %>% recode('0' = "No", '1' = "Yes") %>% as.factor()

 pred_nn_prob <- model %>% predict_proba(test_data) %>% as.data.frame() %>% pull(2)

 confusionMatrix(pred_nn, test_y2, mode = "prec_recall")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  852 120
       Yes 180 253
                                          
               Accuracy : 0.7865          
                 95% CI : (0.7641, 0.8076)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 3.796e-06       
                                          
                  Kappa : 0.4793          
                                          
 Mcnemar's Test P-Value : 0.0006583       
                                          
              Precision : 0.8765          
                 Recall : 0.8256          
                     F1 : 0.8503          
             Prevalence : 0.7345          
         Detection Rate : 0.6064          
   Detection Prevalence : 0.6918          
      Balanced Accuracy : 0.7519          
                                          
       'Positive' Class : No              
                                          

The Balanced Accuracy of the Multilayer Perceptron model is 75.2% which is less than the logistic regression model however it has a much better the F1 score 85.0%and Recall score 82.6%. This is the highest F1 score we have got till now.

6.5 Gradient Boosting Machine (GBM)

set.seed(3000)
cl <- makeCluster(6)
registerDoParallel(cl)

gbmGrid <- expand.grid(interaction.depth = seq(5,10,2), n.trees = c(5:10) * 50, shrinkage = seq(0.01, 0.05, 0.01), n.minobsinnode = seq(10,40,10))

model_gbm <- train(x = train_data, y = train_y2, method = "gbm", trControl = ctrl, metric = metric, tuneGrid = gbmGrid, weights = model_weights)
Iter   TrainDeviance   ValidDeviance   StepSize   Improve
     1        1.3715             nan     0.0200    0.0070
     2        1.3578             nan     0.0200    0.0066
     3        1.3448             nan     0.0200    0.0060
     4        1.3325             nan     0.0200    0.0060
     5        1.3209             nan     0.0200    0.0058
     6        1.3092             nan     0.0200    0.0057
     7        1.2985             nan     0.0200    0.0050
     8        1.2879             nan     0.0200    0.0051
     9        1.2768             nan     0.0200    0.0052
    10        1.2668             nan     0.0200    0.0049
    20        1.1840             nan     0.0200    0.0033
    40        1.0798             nan     0.0200    0.0017
    60        1.0208             nan     0.0200    0.0009
    80        0.9845             nan     0.0200    0.0006
   100        0.9609             nan     0.0200    0.0002
   120        0.9433             nan     0.0200    0.0003
   140        0.9307             nan     0.0200    0.0000
   160        0.9213             nan     0.0200   -0.0000
   180        0.9131             nan     0.0200   -0.0000
   200        0.9063             nan     0.0200   -0.0000
   220        0.9006             nan     0.0200   -0.0000
   240        0.8953             nan     0.0200   -0.0001
   260        0.8903             nan     0.0200   -0.0001
   280        0.8856             nan     0.0200   -0.0001
   300        0.8810             nan     0.0200   -0.0001
   320        0.8764             nan     0.0200   -0.0001
   340        0.8725             nan     0.0200   -0.0001
   350        0.8706             nan     0.0200   -0.0001
cm_gbm <- cm(model_gbm,test_data, test_y2)

print(cm_gbm)
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  755  77
       Yes 277 296
                                          
               Accuracy : 0.748           
                 95% CI : (0.7245, 0.7706)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.1316          
                                          
                  Kappa : 0.4484          
                                          
 Mcnemar's Test P-Value : <2e-16          
                                          
              Precision : 0.9075          
                 Recall : 0.7316          
                     F1 : 0.8101          
             Prevalence : 0.7345          
         Detection Rate : 0.5374          
   Detection Prevalence : 0.5922          
      Balanced Accuracy : 0.7626          
                                          
       'Positive' Class : No              
                                          
stopCluster(cl)
print("Cluster stopped")
[1] "Cluster stopped"
# insert serial backend, otherwise error in summary.connection(connection) : invalid connection
registerDoSEQ()

The Balanced Accuracy of the GBM model is ~76.3% less than the best we have in the logistic regression model ~77.4%.

# Get the probabilitis of prediction
p4 <- predict(model_gbm, test_data, type = "prob")

cutoff <- roc_cutoff(model_gbm, data = test_data, test_y2)


opt_pred_gbm <- ifelse(p4[,2] >= cutoff, "Yes","No")

# Optimized predictions
confusionMatrix(factor(opt_pred_gbm), test_y2, mode = "prec_recall")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  761  77
       Yes 271 296
                                          
               Accuracy : 0.7523          
                 95% CI : (0.7289, 0.7747)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.06863         
                                          
                  Kappa : 0.4554          
                                          
 Mcnemar's Test P-Value : < 2e-16         
                                          
              Precision : 0.9081          
                 Recall : 0.7374          
                     F1 : 0.8139          
             Prevalence : 0.7345          
         Detection Rate : 0.5416          
   Detection Prevalence : 0.5964          
      Balanced Accuracy : 0.7655          
                                          
       'Positive' Class : No              
                                          

By tweaking cutoff value we were able to slightly improve the Balanced Accuracy of the GBM model from ~76.3% to ~76.6% which is still less than best we have so far.

6.6 Model Averaged Neural Network (AvNNET)

set.seed(3000)
cl <- makeCluster(6)
registerDoParallel(cl)

nnGrid <- expand.grid(size = seq(1,3,1), decay = seq(0.0001, 0.001, 0.0001), bag = c(TRUE, FALSE))

model_avNNet <- train(x = train_x2, y = train_y2, method = "avNNet", trControl = ctrl, metric = metric, tuneGrid = nnGrid, weights = model_weights, repeats = 10)


cm_avNNet <- cm(model_avNNet,test_x2, test_y2)

print(cm_avNNet)
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  944 194
       Yes  88 179
                                          
               Accuracy : 0.7993          
                 95% CI : (0.7774, 0.8199)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 9.270e-09       
                                          
                  Kappa : 0.434           
                                          
 Mcnemar's Test P-Value : 4.035e-10       
                                          
              Precision : 0.8295          
                 Recall : 0.9147          
                     F1 : 0.8700          
             Prevalence : 0.7345          
         Detection Rate : 0.6719          
   Detection Prevalence : 0.8100          
      Balanced Accuracy : 0.6973          
                                          
       'Positive' Class : No              
                                          
stopCluster(cl)
print("Cluster stopped")
[1] "Cluster stopped"
# insert serial backend, otherwise error in summary.connection(connection) : invalid connection
registerDoSEQ()

The avNNet is a model where the same neural network model is fit using different random number seeds. All the resulting models are used for prediction. The average Neural Network has the best F1 score till now ~87.0% even better than the Keras model of ~85.0% . However, the Balanced Accuracy has taken a beating ~69.7% . Let us try to optimize it.

# Get the probabilitis of prediction
p5 <- predict(model_avNNet, test_x2, type = "prob")

cutoff <- roc_cutoff(model_avNNet, data = test_x2, test_y2)


opt_pred_avNNet <- ifelse(p5[,2] >= cutoff, "Yes","No")

# Optimized predictions
confusionMatrix(factor(opt_pred_avNNet), test_y2, mode = "prec_recall")
Confusion Matrix and Statistics

          Reference
Prediction  No Yes
       No  755  69
       Yes 277 304
                                          
               Accuracy : 0.7537          
                 95% CI : (0.7303, 0.7761)
    No Information Rate : 0.7345          
    P-Value [Acc > NIR] : 0.05384         
                                          
                  Kappa : 0.464           
                                          
 Mcnemar's Test P-Value : < 2e-16         
                                          
              Precision : 0.9163          
                 Recall : 0.7316          
                     F1 : 0.8136          
             Prevalence : 0.7345          
         Detection Rate : 0.5374          
   Detection Prevalence : 0.5865          
      Balanced Accuracy : 0.7733          
                                          
       'Positive' Class : No              
                                          

By tweaking the cutoff we were able to improve the Balanced Accuracy from ~69.7% to ~77.3% however it came at an expense of the F1 score. Another notable change was in the Specificity of the model which increased from ~48.0% to ~81.5%.

7 Conclusion

There are a wide variety of machine learning algorithms applicable to predicting customer churn. Choosing one model over the other will depend on the questions we are trying to answer. For example, businesses more interested in predicting customers who will churn will likely choose an optimized RF or avNNet model, on the other hand businesses who want to design their strategy to align for customers who are likely to stay may choose unoptimized avNNet model. Yet others may choose a Logistic Regression model to predict both types of customers equally well. To conclude, there is no best model it just depends on what you are trying to answer.

LS0tDQp0aXRsZTogIkN1c3RvbWVyIENodXJuIFByZWRpY3Rpb24iDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGZpZ19oZWlnaHQ6IDEwDQogICAgZmlnX3dpZHRoOiAxMA0KICAgIG51bWJlcl9zZWN0aW9uOiB5ZXMNCiAgICB0aGVtZTogZGVmYXVsdA0KICAgIHRvYzogeWVzDQogICAgdG9jX2NvbGxhcHNlZDogeWVzDQogICAgdG9jX2RlcHRoOiA0DQogICAgdG9jX2Zsb2F0OiB5ZXMNCi0tLQ0KDQpgYGB7ciBnbG9iYWxfb3B0aW9uc30NCmtuaXRyOjpvcHRzX2NodW5rJHNldChmaWcud2lkdGggPSAxMCwgZmlnLmhlaWdodCA9IDEwLCBmaWcucGF0aCA9ICdGaWdzLycsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFKQ0KYGBgDQoNCiMgSW50cm9kdWN0aW9uDQoqKkN1c3RvbWVyIGNodXJuIG9jY3VycyB3aGVuIGN1c3RvbWVycyBvciBzdWJzY3JpYmVycyBzdG9wIGRvaW5nIGJ1c2luZXNzKiogd2l0aCBhIGNvbXBhbnkgb3Igc2VydmljZS4gVGhpcyBpcyBhbHNvIGNhbGxlZCBjdXN0b21lciBhdHRyaXRpb24gb3IgY3VzdG9tZXIgZGVmZWN0aW9uLiBNYW55IHNlcnZpY2UgcHJvdmlkZXJzIHN1Y2ggYXMgdGVsZXBob25lIGNvbXBhbmllcywgaW50ZXJuZXQgb3IgaW5zdXJhbmNlIGZpcm1zLCBiYW5rcyBvZnRlbiB1c2UgY3VzdG9tZXIgY2h1cm4gYW5hbHlzaXMgYXMgb25lIG9mIHRoZSBrZXkgYnVzaW5lc3MgbWV0cmljcyBiZWNhdXNlIHRoZSBjb3N0IG9mIHJldGFpbmluZyBhbiBleGlzdGluZyBjdXN0b21lciBpcyBmYXIgbGVzcyB0aGFuIGFjcXVpcmluZyBuZXcgb25lcy4gDQoNCiMjIEFib3V0IHRoZSBkYXRhDQoNCjEuIFRoZSB0YXJnZXQgdmFyaWFibGUgaXMgY2FsbGVkICpDaHVybiogLSBpdCByZXByZXNlbnRzIGN1c3RvbWVycyB3aG8gbGVmdCB3aXRoaW4gdGhlIGxhc3QgbW9udGguDQoyLiBEZW1vZ3JhcGhpYyBpbmZvcm1hdGlvbiBvZiBjdXN0b21lcnMgaXMgY2FwdHVyZWQgaW4gKmdlbmRlciosICphZ2UqLCAqcGFydG5lcnMqIGFuZCAqZGVwZW5kZW50cyogdmFyaWFibGVzLg0KMy4gQ3VzdG9tZXIgYWNjb3VudCBpbmZvcm1hdGlvbiBpcyBwcmVzZW50IGluICpjb250cmFjdCosICpwYXltZW50IG1ldGhvZCosICpNb250aGx5IGNoYXJnZXMqLCAqdG90YWwgY2hhcmdlcyogYW5kICp0ZW51cmUqLg0KNC4gU2VydmljZXMgb3B0ZWQgYnkgY3VzdG9tZXJzIGFyZSBpbiAqUGhvbmUgc2VydmljZSosICpNdWx0aXBsZSBsaW5lcyosICpPbmxpbmUgYmFja3VwKiwgKlRlY2ggc3VwcG9ydCosICpTdHJlYW1pbmcgTW92aWVzL1RWKi4NCg0KDQojIE9iamVjdGl2ZSANCg0KVGhpcyB0aGUgZGVtb25zdHJhdGlvbiBpbiBSIHRvIGFuYWx5emUgYWxsIHJlbGV2YW50IGN1c3RvbWVyIGRhdGEgYW5kIGNvbXBhcmUgdmFyaW91cyBtYWNoaW5lIGxlYXJuaW5nIG1vZGVscyBpbmNsdWRpbmcgZGVlcCBuZXVyYWwgbmV0d29ya3MgdG8gcHJlZGljdCBjdXN0b21lciBjaHVybi4gIA0KDQojIExvYWQgbGlicmFyaWVzIGFuZCBmdW5jdGlvbnMNCg0KTG9hZCBwYWNrYWdlcyBsaWtlICp0aWR5dmVyc2UqLCAqcHVycnIqLCAqZ3JpZEV4dHJhKiwgKmNhcmV0KiBhbmQgZnVuY3Rpb25zLg0KYGBge3IgbG9hZF9saWJyYXJpZXNfZnVuY3Rpb25zLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSgicmVhZHIiKQ0KbGlicmFyeSgiZHBseXIiKQ0KbGlicmFyeSgidGlkeXIiKQ0KbGlicmFyeSgicHVycnIiKQ0KbGlicmFyeSgiY29ycnBsb3QiKQ0KbGlicmFyeSgic3RyaW5nciIpDQpsaWJyYXJ5KCJST0NSIikNCmxpYnJhcnkoImdncGxvdDIiKQ0KbGlicmFyeSgiZ3JpZEV4dHJhIikNCmxpYnJhcnkoImNhcmV0IikNCmxpYnJhcnkoImthYmxlRXh0cmEiKQ0KbGlicmFyeSgia2VyYXMiKQ0KbGlicmFyeSgiZG9QYXJhbGxlbCIpDQoNCg0KIyBUaGlzIGZ1bmN0aW9uIGNhbGN1bGF0ZXMgdGhlIGNvbmZ1c2lvbiBtYXRyaXgNCmNtIDwtIGZ1bmN0aW9uKG1vZGVsLCBkYXRhLCB0YXJnZXQpew0KY29uZnVzaW9uTWF0cml4KHByZWRpY3QobW9kZWwsIG5ld2RhdGEgPSBkYXRhKSwgdGFyZ2V0LCBtb2RlID0gInByZWNfcmVjYWxsIikNCn0NCg0KIyBUaGlzIGZ1bmN0aW9uIGlkZW50aWZpZXMgYW5kIHBsb3RzIHRoZSBiZXN0IHByb2JhYmlsaXR5IGN1dG9mZiB2YWx1ZXMgYnkgbWF4aW1pemluZyB0cHIgYW5kIGZwciB2YWx1ZXMgZnJvbSB0aGUgUk9DIHBsb3QgDQpyb2NfY3V0b2ZmIDwtIGZ1bmN0aW9uKG1vZGVsLCBkYXRhLCB0YXJnZXQpIHsNCiMgQ2hlY2sgZm9yIHRoZSBzdGFjayBtb2RlbHMgIA0KaWYoc3RyX2RldGVjdChkZXBhcnNlKHN1YnN0aXR1dGUobW9kZWwpKSwnc3RhY2snKSkgew0KcHJlZCA8LSBwcmVkaWN0KG1vZGVsLCBkYXRhLCB0eXBlID0gInByb2IiKSAlPiUgZGF0YS5mcmFtZShObyA9IC4sIFllcyA9IDEgLS4pDQpwcmVkIDwtIHByZWRbLDFdDQp9DQplbHNlew0KcHJlZCA8LSBwcmVkaWN0KG1vZGVsLCBkYXRhLCB0eXBlID0gJ3Byb2InKVssMl0NCn0NCiMgUk9DUiBjb25zaWRlcnMgdGhlIGxhdGVyIGxldmVsIHRvIGJlIHRoZSBwb3NpdGl2ZSBjbGFzcy4gDQpwcmVkIDwtIHByZWRpY3Rpb24ocHJlZCwgdGFyZ2V0LCBsYWJlbC5vcmRlcmluZyA9IGMoIk5vIiwiWWVzIikpDQpldmFsIDwtIHBlcmZvcm1hbmNlKHByZWQsICJ0cHIiLCJmcHIiKQ0KIyBDYWxjdWxhdGUgQVVDIHZhbHVlDQpwcmVmX2F1YyA8LSBwZXJmb3JtYW5jZShwcmVkLCAiYXVjIikNCmF1YyA8LSBwcmVmX2F1Y0B5LnZhbHVlc1tbMV1dDQoNCnBsb3QoZXZhbCkNCg0KIyBtYXhpbWl6ZSB0aGUgVFBSIGFuZCBGUFINCm1heCA8LSB3aGljaC5tYXgoc2xvdChldmFsLCJ5LnZhbHVlcyIpW1sxXV0gKyAgMSAtIHNsb3QoZXZhbCwieC52YWx1ZXMiKVtbMV1dKQ0KIyBnZXQgdGhlIGJlc3QgY3V0b2ZmIHZhbHVlDQpjdXRvZmYgPC0gc2xvdChldmFsLCAiYWxwaGEudmFsdWVzIilbWzFdXVttYXhdDQp0cHIgPC0gc2xvdChldmFsLCAieS52YWx1ZXMiKVtbMV1dW21heF0NCmZwciA8LSBzbG90KGV2YWwsICJ4LnZhbHVlcyIpW1sxXV1bbWF4XQ0KYWJsaW5lKGggPSB0cHIsIHYgPSBmcHIsIGx0eSA9IDIsIGNvbCA9ICJibHVlIikgIyBiZXN0IGN1dG9mZg0KdGV4dCgwLjcsMC4yLCBwYXN0ZTAoIkF0IGJlc3QgY3V0b2ZmID0gIiwgcm91bmQoY3V0b2ZmLDIpKSwgY29sID0gImJsdWUiKQ0KIyBEZWZhdWx0IGN1dG9mZg0KZGVmYXVsdCA8LSBsYXN0KHdoaWNoKHNsb3QoZXZhbCwgImFscGhhLnZhbHVlcyIpW1sxXV0gPj0gMC41KSkNCmRlZmF1bHR5IDwtIHNsb3QoZXZhbCwieS52YWx1ZXMiKVtbMV1dW2RlZmF1bHRdDQpkZWZhdWx0eCA8LSBzbG90KGV2YWwsIngudmFsdWVzIilbWzFdXVtkZWZhdWx0XQ0KYWJsaW5lKGggPSBkZWZhdWx0eSwgdiA9IGRlZmF1bHR4LCBjb2wgPSAicmVkIiwgbHR5ID0gMikgIyBEZWZhdWx0IGN1dG9mZg0KdGV4dCgwLjcsMC4zLCBwYXN0ZTAoIkF0IGRlZmF1bHQgY3V0b2ZmID0gIiwgMC41MCksIGNvbCA9ICJyZWQiKQ0KdGV4dCgwLjcsIDAuNCwgcGFzdGUwKCJBVUMgPSAiLCByb3VuZChhdWMsMikpKQ0KcmV0dXJuKGN1dG9mZikNCn0NCg0KIyBGaW5kIHRoZSBtaXNzaW5nIHZhbHVlcyBpbiB0aGUgY29sdW1ucyBvZiB0aGUgZGF0YWZyYW1lDQptaXNzaW5nX3ZhbHVlcyA8LSBmdW5jdGlvbihkZil7DQogIA0KbWlzc2luZyA8LSBkZiAlPiUgZ2F0aGVyKGtleSA9ICJrZXkiLCB2YWx1ZSA9ICJ2YWx1ZSIpICU+JSBtdXRhdGUoaXMubWlzc2luZyA9IGlzLm5hKHZhbHVlKSkgJT4lIA0KICBncm91cF9ieShrZXkpICU+JSBtdXRhdGUodG90YWwgPSBuKCkpICU+JSANCiAgZ3JvdXBfYnkoa2V5LCB0b3RhbCwgaXMubWlzc2luZykgJT4lDQogICAgc3VtbWFyaXNlKG51bS5taXNzaW5nID0gbigpKSAlPiUgDQogICAgbXV0YXRlKHBlcmMubWlzc2luZyA9IG51bS5taXNzaW5nL3RvdGFsICogMTAwKSAlPiUgdW5ncm91cCgpDQogIA0KICByZXR1cm4obWlzc2luZykNCn0NCg0KIyBQbG90IGFkZCBvbiBzZXJ2aWNlcw0KcGxvdF9hZGRvbl9zZXJ2aWNlcyA8LSBmdW5jdGlvbihkZixncm91cF92YXIpew0KICAjIEZpbHRlciBjdXN0b21lcnMgd2hvIGhhdmUgaW50ZXJuZXQgYXQgaG9tZQ0KICB3aXRoSW50ZXJuZXQgPC0gZGYgJT4lIGZpbHRlcihJbnRlcm5ldFNlcnZpY2UgJW5pJSBjKCJObyIpKQ0KICAjIEdyb3VwIGJ5IENodXJuIGFuZCBhZGQgb24gc2VydmljZSB2YXIgYW5kIGNhbGN1bGF0ZSB0aGUgUGVyY2VudGFnZSBvZiBjdXN0b21lcnMNCiAgd2l0aEludGVybmV0ICU+JSBncm91cF9ieShDaHVybiwgISEgc3ltKGdyb3VwX3ZhcikpICU+JSB0YWxseShuYW1lID0gImNvdW50IikgJT4lIA0KICAgIG11dGF0ZShQZXJjZW50ID0gY291bnQgL25yb3cod2l0aEludGVybmV0KSkgJT4lDQogICAgbXV0YXRlKFBlcmNlbnQgPSByb3VuZChQZXJjZW50LDIpKSAlPiUNCiAgICAjIFBsb3QgdGhlIHBlcmNlbnRhZ2UgYW5kIENodXJuIGJlaGF2aW91ciBvZiBlYWNoIGdyb3VwDQogICAgZ2dwbG90KGFlcyh4ID0gISEgc3ltKGdyb3VwX3ZhciksIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIA0KICAgIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjUsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyANCiAgICB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgDQogICAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkgKyANCiAgICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDkwLCBoanVzdCA9IDEpKQ0KfQ0KDQonJW5pJScgPC0gTmVnYXRlKCclaW4lJykNCmBgYA0KDQojIERhdGEgcHJlcHJvY2Vzc2luZyBhbmQgY2xlYW5pbmcNCg0KVGhlIGRhdGEgd2FzIGRvd25sb2FkZWQgZnJvbSBbS2FnZ2xlXShodHRwczovL3d3dy5rYWdnbGUuY29tL2JsYXN0Y2hhci90ZWxjby1jdXN0b21lci1jaHVybikuIFRoZSBkYXRhIGNvbnRhaW5zICoqNzA0MyByb3dzKiogYW5kICoqMjEgY29sdW1ucyoqICh2YXJpYWJsZXMpIGluY2x1ZGluZyB0aGUgYmluYXJ5IHRhcmdldCB2YXJpYWJsZSBjYWxsZWQgKkNodXJuKi4gDQoNCmBgYHtyIHJlYWRfcHJpbnQsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQoNCmNodXJuIDwtIHJlYWRfY3N2KCJDOlxcVXNlcnNcXGtoYW5udmExXFxEb2N1bWVudHNcXERTLVByb2plY3RzXFx0ZWxjby1jdXN0b21lci1jaHVyblxcV0FfRm4tVXNlQ18tVGVsY28tQ3VzdG9tZXItQ2h1cm4uY3N2IikNCnByaW50KGNodXJuKQ0KYGBgDQoNCjEuIFJlcGxhY2UgKiJObyBpbnRlcm5ldCBzZXJ2aWNlIiogdG8gKiJObyIqLg0KMi4gUmVtb3ZlICoiKGF1dG9tYXRpYykiKiBmcm9tIFBheW1lbnRNZXRob2QgY29sdW1uLg0KMy4gUmVjb2RlICpTZW5pb3JDaXRpemVuKiBjb2x1bW4gdG8gZmFjdG9yLiBNYWtlICIxIiA9ICJZZXMiIGFuZCAiMCIgPSAiTm8iDQo0LiBSZWNvZGUgKnRlbnVyZSogY29sdW1uIHRvIGNhdGVnb3JpY2FsIGNvbHVtbi4NCjUuIFJlbW92ZSBjdXN0b21lcklEIGNvbHVtbiBmcm9tIHRoZSBkYXRhc2V0Lg0KNi4gQ29udmVydCAqY2hhcmFjdGVyKiBjb2x1bW5zIHRvICpmYWN0b3IqIGNvbHVtbnMuDQoNCmBgYHtyIHByZXByb2Nlc3MsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQojIE1hcCAiTm8gaW50ZXJuZXQgc2VydmljZSB0byBObyINCg0KcmVjb2RlIDwtIGMoIk9ubGluZVNlY3VyaXR5IiwgIk9ubGluZUJhY2t1cCIsICJEZXZpY2VQcm90ZWN0aW9uIiwgIlRlY2hTdXBwb3J0IiwgIlN0cmVhbWluZ1RWIiwgIlN0cmVhbWluZ01vdmllcyIsICJNdWx0aXBsZUxpbmVzIikNCg0KY2h1cm4gPC0gYXMuZGF0YS5mcmFtZShjaHVybikNCg0KZm9yIChjb2wgaW4gcmVjb2RlKSB7DQogIGNodXJuWyxjb2xdIDwtIGFzLmNoYXJhY3RlcihjaHVyblssY29sXSkNCiAgdGVtcCA8LSBpZl9lbHNlKGNodXJuWyxjb2xdICVpbiUgYygiTm8gaW50ZXJuZXQgc2VydmljZSIsIk5vIHBob25lIHNlcnZpY2UiKSwgIk5vIixjaHVyblssY29sXSkNCmNodXJuWyxjb2xdIDwtIGFzLmZhY3Rvcih0ZW1wKQ0KfQ0KDQojIFJlbW92ZSAoYXV0b21hdGljKSBmcm9tIFBheW1lbnRNZXRob2QNCmNodXJuJFBheW1lbnRNZXRob2QgPC0gc3RyX3JlbW92ZShjaHVybiRQYXltZW50TWV0aG9kLCAiXFwoYXV0b21hdGljXFwpIikgJT4lIHN0cl90cmltKC4sIHNpZGUgPSAicmlnaHQiKSAlPiUgYXMuZmFjdG9yKCkNCg0KIyBEb2VzIG5vdCBtYWtlIHNlbnNlIHRvIGhhdmUgc2VuaW9yIGNpdGl6ZW4gYXMgbnVtYmVycy4gDQpjaHVybiRTZW5pb3JDaXRpemVuIDwtIGFzLmZhY3RvcihyZWNvZGUoY2h1cm4kU2VuaW9yQ2l0aXplbiwgIjEiID0gIlllcyIsICIwIiA9ICJObyIpKQ0KDQojIE1ha2UgdGVudXJlIGFzIGNhdGVnb3JpY2FsIHZhcmlhYmxlIGZvciBlYXNpZXIgcGxvdGluZyANCmNodXJuIDwtIGNodXJuICU+JSBtdXRhdGUodGVudXJlX2dyb3VwID0gY2FzZV93aGVuKHRlbnVyZSA8PSAxMiB+ICIwLTEyTSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVudXJlID4xMiAmIHRlbnVyZSA8PTI0IH4gIjEyLTI0TSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVudXJlID4gMjQgJiB0ZW51cmUgPD0gNDggfiAiMjQtNDhNIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ZW51cmUgPiA0OCAmIHRlbnVyZSA8PSA2MCB+ICI0OC02ME0iLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlbnVyZSA+NjAgfiAiPjYwTSINCiAgICAgICAgICAgICAgICApKQ0KY2h1cm4kdGVudXJlX2dyb3VwIDwtIGFzLmZhY3RvcihjaHVybiR0ZW51cmVfZ3JvdXApDQoNCiMgUmVtb3ZlIGNvbHVtbnMgbm90IG5lZWRlZA0KDQpjaHVybiA8LSBjaHVybiAlPiUgc2VsZWN0KC1vbmVfb2YoYygiY3VzdG9tZXJJRCIpKSkNCg0KIyBUdXJuIGNoYXJhY3RlciBjb2x1bW5zIHRvIGZhY3Rvcg0KcmVjb2RlIDwtIGNodXJuICU+JSBzZWxlY3RfaWYoaXMuY2hhcmFjdGVyKSAlPiUgY29sbmFtZXMoKQ0KDQpmb3IoY29sIGluIHJlY29kZSl7DQogIGNodXJuWyxjb2xdIDwtICBhcy5mYWN0b3IoY2h1cm5bLGNvbF0pDQoNCn0NCg0KIyMgTG9vayBhdCB0aGUgdW5pcXVlIHZhbHVlcyBhZnRlciBjbGVhbmluZyANCg0KY2h1cm5bLTFdICU+JSBzZWxlY3RfaWYoaXMuZmFjdG9yKSAlPiUgbWFwKGZ1bmN0aW9uKHgpIHVuaXF1ZSh4KSkNCmBgYA0KDQoNCiMjIE1pc3NpbmcgdmFsdWVzDQoNCmBgYHtyIG1pc3NpbmdfdmFsdWVzLCB3YXJuaW5nPUZBTFNFfQ0KbWlzc2luZyA8LSBtaXNzaW5nX3ZhbHVlcyhjaHVybikNCg0KcDEgPC0gZ2dwbG90KG1pc3NpbmcpICsgZ2VvbV9iYXIoYWVzKCB4PSByZW9yZGVyKGtleSwgZGVzYyhwZXJjLm1pc3NpbmcpKSwgeSA9IHBlcmMubWlzc2luZywgZmlsbCA9IGlzLm1pc3NpbmcpLCBzdGF0ID0gJ2lkZW50aXR5JywgYWxwaGEgPSAwLjgpICsgc2NhbGVfZmlsbF9tYW51YWwobmFtZSA9ICIiLCB2YWx1ZXMgPSBjKCdzdGVlbGJsdWUnLCd0b21hdG8zJyksIGxhYmVsID0gYygiUHJlc2VudCIsICJNaXNzaW5nIikpICsgY29vcmRfZmxpcCgpICsgbGFicyh0aXRsZSA9ICJQZXJjZW50YWdlIG9mIG1pc3NpbmcgdmFsdWVzIix4ID0gJycseSA9ICclIG1pc3NpbmcnKQ0KDQojIFBsb3QgbWlzc2luZyB2YWx1ZXMNCnAxDQoNCiMgcmVtb3ZlIHJvd3Mgd2l0aCBtaXNzaW5nIHZhbHVlcw0KY2h1cm4gPC0gbmEub21pdChjaHVybikNCmBgYA0KDQojIEV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXMNCg0KYGBge3IgZWRhfQ0KcHJpbnQoIlBlcmNlbnRhZ2Ugb2YgY3VzdG9tZXJzIGNodXJuIikNCnByb3AudGFibGUodGFibGUoY2h1cm4kQ2h1cm4pKQ0KYGBgDQoNCkZyb20gdGhlIHRhYmxlIGFib3ZlIHdlIG5vdGljZSB0aGF0IDczLjQlIG9mIHRoZSBjdXN0b21lcnMgZGlkIG5vdCBjaHVybi4gVGhpcyBjYW4gc2VydmUgYXMgb3VyIGJhc2VsaW5lIG1vZGVsICppLmUuKiBpZiB3ZSBwcmVkaWN0IGV2ZXJ5IGN1c3RvbWVyIHRvIG5vdCBjaHVybiB3ZSB3aWxsIGJlIHJpZ2h0IG9uIGF2ZXJhZ2UgNzMuNCUgb2YgdGhlIHRpbWUuIExldCB1cyBleHBsb3JlIHRoZSBkYXRhIGZ1cnRoZXIuIA0KDQojIyBDb3JyZWxhdGlvbiBwbG90IGJldHdlZW4gbnVtZXJpYyB2YXJpYWJsZXMNCg0KYGBge3IgcGxvdHN9DQojIENvcnJlbGF0aW9uIG1hdHJpeCBvZiBudW1lcmljIHZhcmlhYmxlcw0KY29yX21hdHJpeDwtIGNodXJuICU+JSBzZWxlY3RfaWYoaXMubnVtZXJpYykgJT4lIGNvcigpDQoNCmNvcnJwbG90KGNvcl9tYXRyaXgsbWV0aG9kID0gIm51bWJlciIsIHR5cGUgPSAidXBwZXIiKQ0KYGBgDQoNCiMjIFBsb3RzIG9mIGNhdGVnb3JpY2FsIHZhcmlhYmxlcw0KDQpgYGB7ciByZWxhdGlvbl9wbG90MSwgZmlnLmhlaWdodD0xMSwgZmlnLndpZHRoPTEzfQ0KDQpwMSA8LSBjaHVybiAlPiUgZ3JvdXBfYnkoZ2VuZGVyLCBDaHVybikgJT4lIHN1bW1hcmlzZShQZXJjZW50ID0gcm91bmQobigpL25yb3coLiksMikpICU+JSBnZ3Bsb3QoYWVzKHggPSBnZW5kZXIsIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjUsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkNCg0KcDIgPC0gY2h1cm4gJT4lIGdyb3VwX2J5KFNlbmlvckNpdGl6ZW4sIENodXJuKSAlPiUgc3VtbWFyaXNlKFBlcmNlbnQgPSByb3VuZChuKCkvbnJvdyguKSwyKSkgJT4lIGdncGxvdChhZXMoeCA9IFNlbmlvckNpdGl6ZW4sIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjAsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkNCg0KcDMgPC0gY2h1cm4gJT4lIGdyb3VwX2J5KFBhcnRuZXIsIENodXJuKSAlPiUgc3VtbWFyaXNlKFBlcmNlbnQgPSByb3VuZChuKCkvbnJvdyguKSwyKSkgJT4lIGdncGxvdChhZXMoeCA9IFBhcnRuZXIsIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjUsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkNCg0KcDQgPC0gY2h1cm4gJT4lIGdyb3VwX2J5KERlcGVuZGVudHMsIENodXJuKSAlPiUgc3VtbWFyaXNlKFBlcmNlbnQgPSByb3VuZChuKCkvbnJvdyguKSwyKSkgJT4lIGdncGxvdChhZXMoeCA9IERlcGVuZGVudHMsIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjUsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkNCg0KcDUgPC0gY2h1cm4gJT4lIGdyb3VwX2J5KFBob25lU2VydmljZSwgQ2h1cm4pICU+JSBzdW1tYXJpc2UoUGVyY2VudCA9IHJvdW5kKG4oKS9ucm93KC4pLDIpKSAlPiUgZ2dwbG90KGFlcyh4ID0gUGhvbmVTZXJ2aWNlLCB5ID0gUGVyY2VudCwgZmlsbCA9IENodXJuKSkgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBnZW9tX3RleHQoYWVzKGxhYmVsID0gUGVyY2VudCAqIDEwMCksIHZqdXN0ID0gMS4wLCBoanVzdCA9IDAuNSxjb2xvciA9ICJ3aGl0ZSIsIHNpemUgPSA1LjApICsgdGhlbWVfbWluaW1hbCgpICsgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KGFjY3VyYWN5ID0gMSkpDQoNCnA2IDwtIGNodXJuICU+JSBncm91cF9ieShNdWx0aXBsZUxpbmVzLCBDaHVybikgJT4lIHN1bW1hcmlzZShQZXJjZW50ID0gcm91bmQobigpL25yb3coLiksMikpICU+JSBnZ3Bsb3QoYWVzKHggPSBNdWx0aXBsZUxpbmVzLCB5ID0gUGVyY2VudCwgZmlsbCA9IENodXJuKSkgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBnZW9tX3RleHQoYWVzKGxhYmVsID0gUGVyY2VudCAqIDEwMCksIHZqdXN0ID0gMS41LCBoanVzdCA9IDAuNSxjb2xvciA9ICJ3aGl0ZSIsIHNpemUgPSA1LjApICsgdGhlbWVfbWluaW1hbCgpICsgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KGFjY3VyYWN5ID0gMSkpDQoNCnA3IDwtIGNodXJuICU+JSBncm91cF9ieShJbnRlcm5ldFNlcnZpY2UsIENodXJuKSAlPiUgc3VtbWFyaXNlKFBlcmNlbnQgPSByb3VuZChuKCkvbnJvdyguKSwyKSkgJT4lIGdncGxvdChhZXMoeCA9IEludGVybmV0U2VydmljZSwgeSA9IFBlcmNlbnQsIGZpbGwgPSBDaHVybikpICsgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IFBlcmNlbnQgKiAxMDApLCB2anVzdCA9IDEuNSwgaGp1c3QgPSAwLjUsY29sb3IgPSAid2hpdGUiLCBzaXplID0gNS4wKSArIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGU9IkRhcmsyIikgKyBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpKQ0KDQpwOCA8LSBjaHVybiAlPiUgZ3JvdXBfYnkoT25saW5lU2VjdXJpdHksIENodXJuKSAlPiUgc3VtbWFyaXNlKFBlcmNlbnQgPSByb3VuZChuKCkvbnJvdyguKSwyKSkgJT4lIGdncGxvdChhZXMoeCA9IE9ubGluZVNlY3VyaXR5LCB5ID0gUGVyY2VudCwgZmlsbCA9IENodXJuKSkgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBnZW9tX3RleHQoYWVzKGxhYmVsID0gUGVyY2VudCAqIDEwMCksIHZqdXN0ID0gMS41LCBoanVzdCA9IDAuNSxjb2xvciA9ICJ3aGl0ZSIsIHNpemUgPSA1LjApICsgdGhlbWVfbWluaW1hbCgpICsgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KGFjY3VyYWN5ID0gMSkpDQoNCmdyaWQuYXJyYW5nZShwMSwgcDIsIHAzLCBwNCwgcDUsIHA2LCBwNywgcDgsIG5jb2w9NCkNCmBgYA0KDQoxLiAqKkRvZXMgZ2VuZGVyIG1ha2VzIGEgZGlmZmVyZW5jZT8qKiBJdCBkb2VzIG5vdCBzZWVtIHRvIHBsYXkgYW55IHNpZ25pZmljYW50IHJvbGUgaW4gcHJlZGljdGluZyBjdXN0b21lciBjaHVybi4NCg0KMi4gKipIb3cgZG9lcyBTZW5pb3IgQ2l0aXplbiBiZWhhdmU/KiogQWxtb3N0IDUwJSBvZiB0aGUgdG90YWwgU2VuaW9yIGNpdGl6ZW4ncyBwb3B1bGF0aW9uIGNodXJuLiBBIGNvbXBhbnkgY291bGQgY29tZSB1cCB3aXRoIG9mZmVycyBzcGVjaWZpY2FsbHkgY2F0ZXJpbmcgZm9yIHNlbmlvciBjaXRpemVuIGRlbWFuZHMuDQoNCjMuICoqRG9lcyBoYXZpbmcgYSBwYXJ0bmVyIG1ha2VzIGEgZGlmZmVyZW5jZT8qKiBJdCBzZWVtcyBsaWtlIGEgY3VzdG9tZXIgd2l0aG91dCBhIHBhcnRuZXIgaXMgbW9yZSBsaWtlbHkgdG8gY2h1cm4gdGhhbiBjdXN0b21lcnMgd2l0aCB0aGUgcGFydG5lci4NCg0KNC4gKipFZmZlY3Qgb2YgZGVwZW5kZW50cz8qKiBDdXN0b21lcnMgd2l0aCBkZXBlbmRlbnRzIGFyZSBtb3JlIGxpa2VseSB0byBzdGF5IHRoYW4gd2l0aG91dC4gSG93ZXZlciwgY3VzdG9tZXJzIHdpdGggZGVwZW5kZW50cyBhcmUgb25seSAzMCUgb2YgdGhlIHRvdGFsIGN1c3RvbWVyIHBvcHVsYXRpb24uDQoNCjUuICoqUGhvbmUgc2VydmljZSoqIEN1c3RvbWVycyB3aXRob3V0IHBob25lIHNlcnZpY2UgYXJlIGxlc3MgbGlrZWx5IHRvIGNodXJuIGhvd2V2ZXIsIHRoZXkgZm9ybSBvbmx5IDklIG9mIHRoZSB0b3RhbCBjdXN0b21lcnMuDQoNCjYuICoqTXVsdGlwbGUgbGluZXMqKiBJIGRvbid0IGtub3cgd2hhdCBtdWx0aXBsZSBsaW5lcyBtZWFuLiBIb3dldmVyLCBpdCBzZWVtcyB0aGF0IGN1c3RvbWVycyB3aXRoIG11bHRpcGxlIGxpbmVzIGFyZSBtb3JlIGxpa2VseSB0byBjaHVybi4gDQoNCjcuICoqQ3VzdG9tZXJzIHdpdGggSW50ZXJuZXQgc2VydmljZXMqKiBJdCBpcyB2ZXJ5IGludGVyZXN0aW5nIHRvIHNlZSB0aGF0IGN1c3RvbWVycyB3aXRoIEZpYmVyIG9wdGljIGludGVybmV0IHNlcnZpY2UgYXQgaG9tZSBhcmUgaGlnaGx5IHByb25lIHRvIGNodXJuaW5nLiANCg0KOC4gKipDdXN0b21lcnMgd2l0aCBPbmxpbmUgc2VjdXJpdHkqKiBDdXN0b21lcnMgd2l0aCBvbmxpbmUgc2VjdXJpdHkgYXJlIG1vcmUgbGlrZWx5IHRvIHN0YXkuDQoNCmBgYHtyIHJlbGF0aW9uX3Bsb3QyLCBmaWcuaGVpZ2h0PTExLCBmaWcud2lkdGg9MTN9DQpwMSA8LSBjaHVybiAlPiUgZ3JvdXBfYnkoT25saW5lQmFja3VwLCBDaHVybikgJT4lIHN1bW1hcmlzZShQZXJjZW50ID0gcm91bmQobigpL25yb3coLiksMikpICU+JSBnZ3Bsb3QoYWVzKHggPSBPbmxpbmVCYWNrdXAsIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjUsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkNCg0KDQpwMiA8LSBjaHVybiAlPiUgZ3JvdXBfYnkoRGV2aWNlUHJvdGVjdGlvbiwgQ2h1cm4pICU+JSBzdW1tYXJpc2UoUGVyY2VudCA9IHJvdW5kKG4oKS9ucm93KC4pLDIpKSAlPiUgZ2dwbG90KGFlcyh4ID0gRGV2aWNlUHJvdGVjdGlvbiwgeSA9IFBlcmNlbnQsIGZpbGwgPSBDaHVybikpICsgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IFBlcmNlbnQgKiAxMDApLCB2anVzdCA9IDEuNSwgaGp1c3QgPSAwLjUsY29sb3IgPSAid2hpdGUiLCBzaXplID0gNS4wKSArIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGU9IkRhcmsyIikgKyBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpKQ0KDQpwMyA8LSBjaHVybiAlPiUgZ3JvdXBfYnkoVGVjaFN1cHBvcnQsIENodXJuKSAlPiUgc3VtbWFyaXNlKFBlcmNlbnQgPSByb3VuZChuKCkvbnJvdyguKSwyKSkgJT4lIGdncGxvdChhZXMoeCA9IFRlY2hTdXBwb3J0LCB5ID0gUGVyY2VudCwgZmlsbCA9IENodXJuKSkgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBnZW9tX3RleHQoYWVzKGxhYmVsID0gUGVyY2VudCAqIDEwMCksIHZqdXN0ID0gMS41LCBoanVzdCA9IDAuNSxjb2xvciA9ICJ3aGl0ZSIsIHNpemUgPSA1LjApICsgdGhlbWVfbWluaW1hbCgpICsgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KGFjY3VyYWN5ID0gMSkpDQoNCnA0IDwtIGNodXJuICU+JSBncm91cF9ieShTdHJlYW1pbmdUViwgQ2h1cm4pICU+JSBzdW1tYXJpc2UoUGVyY2VudCA9IHJvdW5kKG4oKS9ucm93KC4pLDIpKSAlPiUgZ2dwbG90KGFlcyh4ID0gU3RyZWFtaW5nVFYsIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjUsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkNCg0KcDUgPC0gY2h1cm4gJT4lIGdyb3VwX2J5KFN0cmVhbWluZ01vdmllcywgQ2h1cm4pICU+JSBzdW1tYXJpc2UoUGVyY2VudCA9IHJvdW5kKG4oKS9ucm93KC4pLDIpKSAlPiUgZ2dwbG90KGFlcyh4ID0gU3RyZWFtaW5nTW92aWVzLCB5ID0gUGVyY2VudCwgZmlsbCA9IENodXJuKSkgKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikgKyBnZW9tX3RleHQoYWVzKGxhYmVsID0gUGVyY2VudCAqIDEwMCksIHZqdXN0ID0gMS41LCBoanVzdCA9IDAuNSxjb2xvciA9ICJ3aGl0ZSIsIHNpemUgPSA1LjApICsgdGhlbWVfbWluaW1hbCgpICsgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnRfZm9ybWF0KGFjY3VyYWN5ID0gMSkpDQoNCnA2IDwtIGNodXJuICU+JSBncm91cF9ieShDb250cmFjdCwgQ2h1cm4pICU+JSBzdW1tYXJpc2UoUGVyY2VudCA9IHJvdW5kKG4oKS9ucm93KC4pLDIpKSAlPiUgZ2dwbG90KGFlcyh4ID0gQ29udHJhY3QsIHkgPSBQZXJjZW50LCBmaWxsID0gQ2h1cm4pKSArIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArIGdlb21fdGV4dChhZXMobGFiZWwgPSBQZXJjZW50ICogMTAwKSwgdmp1c3QgPSAxLjUsIGhqdXN0ID0gMC41LGNvbG9yID0gIndoaXRlIiwgc2l6ZSA9IDUuMCkgKyB0aGVtZV9taW5pbWFsKCkgKyBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoYWNjdXJhY3kgPSAxKSkgKyB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDkwLCBoanVzdCA9IDEpKQ0KDQpwNyA8LSBjaHVybiAlPiUgZ3JvdXBfYnkoUGFwZXJsZXNzQmlsbGluZywgQ2h1cm4pICU+JSBzdW1tYXJpc2UoUGVyY2VudCA9IHJvdW5kKG4oKS9ucm93KC4pLDIpKSAlPiUgZ2dwbG90KGFlcyh4ID0gUGFwZXJsZXNzQmlsbGluZywgeSA9IFBlcmNlbnQsIGZpbGwgPSBDaHVybikpICsgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IFBlcmNlbnQgKiAxMDApLCB2anVzdCA9IDEuNSwgaGp1c3QgPSAwLjUsY29sb3IgPSAid2hpdGUiLCBzaXplID0gNS4wKSArIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGU9IkRhcmsyIikgKyBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpKQ0KDQpwOCA8LSBjaHVybiAlPiUgZ3JvdXBfYnkoUGF5bWVudE1ldGhvZCwgQ2h1cm4pICU+JSBzdW1tYXJpc2UoUGVyY2VudCA9IHJvdW5kKG4oKS9ucm93KC4pLDIpKSAlPiUgZ2dwbG90KGFlcyh4ID0gUGF5bWVudE1ldGhvZCwgeSA9IFBlcmNlbnQsIGZpbGwgPSBDaHVybikpICsgZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpICsgZ2VvbV90ZXh0KGFlcyhsYWJlbCA9IFBlcmNlbnQgKiAxMDApLCB2anVzdCA9IDEuNSwgaGp1c3QgPSAwLjUsY29sb3IgPSAid2hpdGUiLCBzaXplID0gNS4wKSArIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGU9IkRhcmsyIikgKyBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdChhY2N1cmFjeSA9IDEpKSArIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gOTAsIGhqdXN0ID0gMSkpDQoNCmdyaWQuYXJyYW5nZShwMSwgcDIsIHAzLCBwNCwgcDUsIHA2LCBwNywgcDgsIG5jb2w9NCkNCg0KYGBgDQoNCldoYXQgaXMgdGhlIGVmZmVjdCBvZiBpbnRlcm5ldCBhZGRvbnMgb24gY3VzdG9tZXIgY2h1cm5pbmc/IExldCB1cyBleHBsb3JlLg0KDQoxLiAqKk9ubGluZSBiYWNrdXAgYW5kIERldmljZSBQcm90ZWN0aW9uIHNlcnZpY2U6KiogQ3VzdG9tZXJzIHdpdGggb25saW5lIGJhY2t1cCBhbmQgZGV2aWNlIHByb3RlY3Rpb24gc2VydmljZXMgYXJlIGxlc3MgbGlrZWx5IHRvIGNodXJuLiBUaGV5IGxpa2UgdGhlc2UgYWRkIG9ucy4gDQoNCjIuICoqVGVjaG5pY2FsIHN1cHBvcnQqKiBDdXN0b21lcnMgd2hvIG9wdCBmb3IgdGVjaG5pY2FsIHN1cHBvcnQgYXJlIGxlc3MgbGlrZWx5IHRvIGNodXJuLiBBbHRob3VnaCBmZXdlciBjdXN0b21lcnMgb3B0IGZvciB0ZWNobmljYWwgc3VwcG9ydCBzZXJ2aWNlIGhvd2V2ZXIgaWYgdGhleSBkbyB0aGV5IHNlZW0gdG8gc3RheSB0aGUgY29tcGFueS4NCg0KMy4gKipEb2VzIFN0cmVhbWluZyBzZXJ2aWNlcyBoZWxwIHJldGFpbiBjdXN0b21lcnM/KiogSSB3b3VsZCB0aGlrIHRoYXQgdGhvc2UgY3VzdG9tZXJzIHdobyBvcHQgZm9yIHN0cmVhbWluZyBzZXJ2aWNlcyBhcmUgbW9yZSBsaWtlbHkgdG8gc3RheSBpbnRlcmVzdGluZ2x5LCBpdCBpcyB0aGUgb3Bwb3NpdGUgdGhleSBhcmUgbW9yZSBwcm9uZSB0byBjaHVybmluZy4NCg0KNC4gKipXaGF0IGFib3V0IENvbnRyYWN0IHBlcmlvZD8qKiBWZXJ5IGludGVyZXN0aW5nIHBhdHRlcm4uIElmIHRoZSBjb21wYW55IGNhbiBlbmdhZ2UgY3VzdG9tZXJzIGluIDEgeWVhciBvciBtb3JlIGNvbnRyYWN0IGN1c3RvbWVycyBhcmUgYWxtb3N0IGNlcnRhaW4gdG8gc3RheS4NCg0KNS4gKipQYXBlcmxlc3MgYmlsbGluZyoqLiBJdCBpcyBpbnRlcmVzdGluZyB0byBzZWUgdGhhdCBjdXN0b21lcnMgd2hvIG9wdCBmb3IgcGFwZXJsZXNzIGJpbGxpbmcgc2VydmljZSBhcmUgbW9yZSBsaWtlbHkgdG8gY2h1cm4uIEl0IG1heSBiZSBiZWNhdXNlIHBlb3BsZSB0ZW5kIHRvIGZvcmdldCBwYXlpbmcgYmlsbHMgYW5kIGlmIHRoZSBiaWxsIGlzIGhpZGluZyBzb21ld2hlcmUgaW4gdGhlaXIgZW1haWwgb3IgU01TIGl0IGJlY29tZXMgZXZlbiBtb3JlIGRpZmZpY3VsdCB0byByZW1lbWJlciBhbmQgcGF5IG9uIHRpbWUsIHRoZXJlZm9yZSwgbW9zdCBsaWtlbHkgdGhleSBtaXNzIHRoZSBkdWUgZGF0ZSBhbmQgZW5kIHVwIHBheWluZyBleHRyYSB3aGljaCBtYWtlcyB0aGVtIHVuaGFwcHkgd2l0aCB0aGUgY29tcGFueSBhbmQgdGhleSBjaHVybi4NCg0KNi4gKipQYXltZW50IG1ldGhvZCoqIFZlcnkgaW50ZXJlc3RpbmcgcGF0dGVybi4gT2YgYWxsIHRoZSBjdXN0b21lcnMgcGF5aW5nIGJpbGxzIHRocm91Z2ggZWxlY3Ryb25pYyBjaGVjayBhbG1vc3QgaGFsZiBvZiB0aGUgY3VzdG9tZXJzIGNodXJuLiBJdCBtYXkgYmUgZHVlIHRvIHRoZSBiYWQgZXhwZXJpZW5jZSBsaWtlIGJvdW5jaW5nIG9mIGNoZWNrcyBvciBzb21lIGVycm9yIG9uIHRoZSBjb21wYW55J3MgcGFydCBpbiBwcm9jZXNzaW5nIGNoZWNrIHBheW1lbnRzLiANCg0KDQojIyBTb21lIG90aGVyIGludGVyZXN0aW5nIHBsb3RzDQoNCkxldCB1cyB0YWtlIGEgKipkZWVwZXIgbG9vayoqIGF0IHRoZSBwb3B1bGFyaXR5IG9mIGFkZCBvbiBzZXJ2aWNlcyBzdWNoIGFzICpUZWNoIFN1cHBvcnQqLCAqU3RyZWFtaW5nIE1vdmllcyosICpPbmxpbmUgYmFja3VwKiwgZXRjLiBmb3IgdGhlICoqY3VzdG9tZXJzIHdobyBoYXZlIHRoZSBpbnRlcm5ldCBhdCBob21lKiouIEZvciB0aGlzLCB3ZSB3aWxsIGZpbHRlciBvdXQgY3VzdG9tZXJzIHdpdGggdGhlIGludGVybmV0IGF0IGhvbWUuDQpgYGB7ciBhZGRvbl9wbG90cywgZmlnLmhlaWdodD0xMSwgZmlnLndpZHRoPTEzfQ0KIyBDcmVhdGUgYW4gYWRkcyBvbiB2ZWN0b3INCmFkZHNfb24gPC0gbmFtZXMoY2h1cm4pWzc6MTRdDQoNCmFkZHNvbl9wbG90cyA8LSBtYXAoYWRkc19vbiwgcGxvdF9hZGRvbl9zZXJ2aWNlcywgZGYgPSBjaHVybikNCg0KcDEgPC0gYWRkc29uX3Bsb3RzW1sxXV0gKyBnZ3RpdGxlKCJQb3B1bGFyaXR5IG9mIGFkZCBvbiBzZXJ2aWNlcyIpDQpwMiA8LSBhZGRzb25fcGxvdHNbWzJdXQ0KcDMgPC0gYWRkc29uX3Bsb3RzW1szXV0NCnA0IDwtIGFkZHNvbl9wbG90c1tbNF1dDQpwNSA8LSBhZGRzb25fcGxvdHNbWzVdXQ0KcDYgPC0gYWRkc29uX3Bsb3RzW1s2XV0NCnA3IDwtIGFkZHNvbl9wbG90c1tbN11dDQpwOCA8LSBhZGRzb25fcGxvdHNbWzhdXQ0KDQpncmlkLmFycmFuZ2UocDEsIHAyLCBwMywgcDQsIHA1LCBwNiwgcDcsIG5jb2w9NCkNCmBgYA0KDQoxLiAqKkludGVybmV0IHNlcnZpY2U6KiogQ3VzdG9tZXJzIHdpdGggRmliZXIgb3B0aWMgc2VydmljZSBhcmUgbW9yZSBwcm9uZSB0byBjaHVybmluZyB0aGFuIERTTCBzZXJ2aWNlLiBJdCBtYXkgYmUgYmVjYXVzZSBEU0wgc2VydmljZSBtYXkgbm90IGJlIGZhc3QgZW5vdWdoIGFuZCBjdXN0b21lcnMgdXNlIHRlbGNvJ3MgaW50ZXJuZXQgYW5kIHRoZXJlZm9yZSwgYXJlIGxlc3MgbGlrZWx5IHRvIGNodXJuLg0KDQoyLiAqKk9ubGluZSBzZWN1cml0eSAmIE9ubGluZSBiYWNrdXA6KiogQ3VzdG9tZXJzIHdobyBkbyBub3QgaGF2ZSBPbmxpbmUgc2VjdXJpdHkgb3IgYmFja3VwIHNlcnZpY2UgYXJlIG1vcmUgbGlrZWx5IHRvIGNodXJuLg0KDQozLiAqKkRldmljZSBQcm90ZWN0aW9uIGFuZCBUZWNoIFN1cHBvcnQ6KiogU2ltaWxhcmx5IGN1c3RvbWVycyB3aG8gZG8gbm90IG9wdCBmb3IgZGV2aWNlIHByb3RlY3Rpb24gYW5kIHRlY2ggc3VwcG9ydCBhcmUgbW9yZSBsaWtlbHkgdG8gY2h1cm4uDQoNCjQuICoqU3RyZWFtaW5nIHNlcnZpY2VzOioqIFN0cmVhbWluZyBzZXJ2aWNlcyBkbyBub3Qgc2lnbmlmaWNhbnRseSBtYWtlIGEgZGlmZmVyZW5jZSBpbiBjdXN0b21lciBjaHVybiBiZWhhdmlvdXIuDQoNCkFkZGl0aW9uYWxseSwgd2UgYWxzbyBzZWUgdGhhdCBwb3B1bGFyaXR5IG9mIHNlcnZpY2VzIHdpdGggY3VzdG9tZXJzIGlzIGluIHRoZSBvcmRlciBTdHJlYW1pbmdNb3ZpZXMgKDQ5LjUlKSA+IFN0cmVhbWluZ1RWICg0OS4wJSkgPiBPbmxpbmVCYWNrdXAgKDQ0LjAlKSA+IERldmljZSBQcm90ZWN0aW9uICg0My45JSkgPiBUZWNoIHN1cHBvcnQgKDM3LjAlKSA+IE9ubGluZSBzZWN1cml0eSAoMzYuNiUpLg0KDQojIyBNb250aGx5IGNoYXJnZXMgcGFpZCBieSBjdXN0b21lcnMNCg0KTGV0IHVzIGxvb2sgYXQgaG93IG11Y2ggb24gYXZlcmFnZSBtb250aGx5IGNoYXJnZXMgYXJlIHBhaWQgYnkgY3VzdG9tZXJzIHdobyBjaHVybi4gTXkgZ3Vlc3MgaXMgY3VzdG9tZXJzIHdobyBjaHVybiBhcmUgcGF5aW5nIGhpZ2hlciBtb250aGx5IGNoYXJnZXMgdGhhbiBjdXN0b21lcnMgd2hvIGRvIG5vdCBjaHVybi4gDQoNCmBgYHtyIG1vbnRobHlfY2hhcmdlc19hdmcsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD0xMn0NCmRmIDwtIGNodXJuICU+JSBncm91cF9ieShnZW5kZXIsIHRlbnVyZV9ncm91cCwgQ2h1cm4pICU+JSANCiAgIHN1bW1hcmlzZShtZWFuID0gbWVkaWFuKE1vbnRobHlDaGFyZ2VzKSkgJT4lIGFycmFuZ2UodGVudXJlX2dyb3VwKQ0KIyBEZWZpbmUgdGhlIHggcG9zaXRpb25zIGZvciBwbG90dGluZyB0ZXh0DQpkZiR4IDwtIHJlcChjKDAuOCwxLjIsMS44LDIuMiksNSkNCg0KY2h1cm4gJT4lIGdncGxvdChhZXMoeSA9IE1vbnRobHlDaGFyZ2VzLCAgeD0gZ2VuZGVyLCBmaWxsID0gQ2h1cm4pKSArIA0KICAgIGdlb21fYm94cGxvdCgpICsgZmFjZXRfd3JhcCh+dGVudXJlX2dyb3VwKSArIA0KICBnZW9tX3RleHQoZGF0YSA9IGRmLCBhZXMobGFiZWwgPSByb3VuZChtZWFuLCAxKSwgeSA9IG1lYW4gKyAzLjAsIHggPSB4KSwgc2l6ZSA9IDQpICsgDQogIHRoZW1lX21pbmltYWwoKSArIHNjYWxlX2ZpbGxfYnJld2VyKHBhbGV0dGU9IkRhcmsyIikNCmBgYA0KDQoxLiAqKkN1c3RvbWVycyB3aG8gY2h1cm4gcGF5IG11Y2ggaGlnaGVyIG1vbnRobHkgYmlsbHMgb24gYXZlcmFnZSoqIHdoaWNoIG1ha2VzIHNlbnNlIHNpbmNlIGhpZ2hlciBtb250aGx5IGJpbGxzIHdvdWxkIGJlIGhhcmQgb24gdGhlaXIgcG9ja2V0LiBJdCBhbHNvIHNlZW1zIHRvIGJlIGEgdmVyeSBkaXN0aW5ndWlzaGluZyBmZWF0dXJlIG9mIHBlb3BsZSB3aG8gY2h1cm4uDQoNCjIuIEFub3RoZXIgaW50ZXJlc3RpbmcgcGF0dGVybiB3aGljaCB3ZSBjYW4gc2VlIGZyb20gdGhlIHBsb3QgYWJvdmUgaXMgdGhhdCAqKmFzIG1vbnRocyBwYXNzIGJ5IGN1c3RvbWVycyB3aG8gZG8gbm90IGNodXJuIGFyZSBoYXBweSB0byBwYXkgbW9yZSoqLiBGb3IgZXhhbXBsZSB0aGUgbWVkaWFuIG1vbnRobHkgYmlsbHMgcmlzZSBpbiB0aGUgb3JkZXIgNDUuNSw1NS41LDYxLjUsNzUuOCw4NC42IGZvciBmZW1hbGUgY3VzdG9tZXJzIGZvciB0ZW51cmVzIDAtMTJNLCAxMi0yNE0sIDI0LTQ4TSwgNDgtNjBNIGFuZCA+NjBNLCByZXNwZWN0aXZlbHkuDQoNCmBgYHtyIHBheV9wcmVmX292ZXJhbGwsIGZpZy53aWR0aD05LCBmaWcuaGVpZ2h0PTExLCBtZXNzYWdlPUZBTFNFfQ0KY2h1cm4kSW50ZXJuZXQgPC0gaWZlbHNlKGNodXJuJEludGVybmV0U2VydmljZSAlaW4lIGMoIk5vIiksICJObyIsICJZZXMiKQ0KDQpjaHVybiAlPiUgZ2dwbG90KGFlcyh4ID0gTW9udGhseUNoYXJnZXMsIGZpbGwgPSBJbnRlcm5ldCkpICsgDQogICAgZmFjZXRfd3JhcCh+UGF5bWVudE1ldGhvZCkgKyB0aGVtZV9taW5pbWFsKCkgKyANCiAgICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgDQogICAgZ2d0aXRsZShsYWJlbCA9ICJNb250aGx5IENoYXJnZXMgcGFpZCBieSBjdXN0b21lcnMgdGhyb3VnaCB2YXJpb3VzIHBheW1lbnQgbWV0aG9kcyB3aXRoIG9yIHdpdGhvdXQgaW50ZXJuZXQiKSArDQogICAgeWxhYihsYWJlbCA9ICJDb3VudCIpICsgZ2VvbV9oaXN0b2dyYW0oKQ0KDQpgYGANCg0KRnJvbSB0aGUgcGxvdCBhYm92ZSB3ZSBjYW4gc2VlIHRoYXQgKiptYWpvcml0eSBvZiBjdXN0b21lcnMgd2hvIGRvIG5vdCBoYXZlIGludGVybmV0IHNlcnZpY2UgYXQgaG9tZSBwcmVmZXIgdG8gcGF5IGJpbGxzIGJ5IE1haWxlZCBjaGVjayoqICh3aGljaCBpcyBleHBlY3RlZCkuIEFkZGl0aW9uYWxseSwgc3VjaCBjdXN0b21lcnMgdXN1YWxseSBoYXZlIGEgc21hbGwgYW1vdW50IHRvIGJlIHBhaWQuDQoNCkFsc28sIHdlIGNhbiBub3RlIHRoYXQgKiptb3N0IGNvbW1vbiBwYXltZW50IG1ldGhvZCBvZiBjdXN0b21lcnMgd2hvIGhhdmUgaW50ZXJuZXQgc2VydmljZSBhdCBob21lIGlzIEVsZWN0cm9uaWMgY2hlY2sqKiBhbHRob3VnaCwgKkJhbmsgdHJhbnNmZXIgYW5kIENyZWRpdCBjYXJkKiBhcmUgYWxzbyBwb3B1bGFyIG1lYW5zIG9mIHBheW1lbnQuIA0KDQojIyBTZW5pb3IgQ2l0aXplbiBjdXN0b21lcnMNCmBgYHtyIHBheV9wcmVmMiwgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9MTAsIG1lc3NhZ2U9RkFMU0V9DQoNCiAgY2h1cm4gJT4lIGdncGxvdChhZXMoeCA9IE1vbnRobHlDaGFyZ2VzLCBmaWxsID0gSW50ZXJuZXQpKSArIA0KICAgIGdlb21faGlzdG9ncmFtKCkgKyBmYWNldF9ncmlkKFNlbmlvckNpdGl6ZW5+UGF5bWVudE1ldGhvZCkgKyB0aGVtZV9taW5pbWFsKCkgKyANCiAgICBzY2FsZV9maWxsX2JyZXdlcihwYWxldHRlPSJEYXJrMiIpICsgDQogICAgZ2d0aXRsZShsYWJlbCA9ICJEaXN0cmlidXRpb24gb2YgTW9udGhseSBDaGFyZ2VzIHBhaWQgYnkgU2VuaW9yIENpdGl6ZW4gQ3VzdG9tZXJzIikgKw0KICAgIHlsYWIobGFiZWwgPSAiQ291bnQiKQ0KYGBgDQoNCkl0IGxvb2tzIGxpa2Ugb3ZlcmFsbCAqKk1haWxlZCBjaGVja3MgYWdhaW4gYXJlIHRoZSBtb3N0IGNvbW1vbiB0eXBlIG9mIHBheW1lbnQgbWV0aG9kIGZvciBwYXlpbmcgc21hbGwgbW9udGhseSBiaWxscyoqIGJ5IGN1c3RvbWVycy4gSXQgaXMgaW50ZXJlc3RpbmcgdG8gbm90ZSB0aGF0IFNlbmlvciBDaXRpemVucyBkbyBub3QgbGlrZSB0byBtYWlsIGNoZWNrcyByYXRoZXIgcHJlZmVyIHRvIHBheSBieSBlbGVjdHJvbmljIGNoZWNrIHdoaWNoIGlzIHF1aXRlIGludGVyZXN0aW5nIHRvIGtub3cgYmVjYXVzZSBvbmUgd291bGQgdGhpbmsgdGhhdCBzZW5pb3IgY2l0aXplbnMgd291bGQgYmUgbW9yZSBvbGQgc2Nob29sLg0KDQojIyBNb250aGx5IGNoYXJnZXMgYnkgdGVudXJlIGdyb3VwDQoNCmBgYHtyIHRnLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD0xMCwgbWVzc2FnZT1GQUxTRX0NCmNodXJuICU+JSBncm91cF9ieShDaHVybikgJT4lIGdncGxvdChhZXMoeCA9IE1vbnRobHlDaGFyZ2VzLCBmaWxsID0gQ2h1cm4pKSArIGdlb21faGlzdG9ncmFtKCkgKyBmYWNldF93cmFwKH50ZW51cmVfZ3JvdXApICsgdGhlbWVfbWluaW1hbCgpICsgc2NhbGVfZmlsbF9icmV3ZXIocGFsZXR0ZT0iRGFyazIiKSArIGdndGl0bGUobGFiZWwgPSAiTW9udGhseSBDaGFyZ2VzIHBhaWQgYnkgQ3VzdG9tZXJzIG92ZXIgdGhlIHRlbnVyZSAiKSArIHlsYWIobGFiZWwgPSAiQ291bnQiKQ0KYGBgDQoNCkZyb20gdGhlIHBsb3QgYWJvdmUgaXQgbG9va3MgbGlrZSAqKmN1c3RvbWVycyBtb3N0bHkgY2h1cm4gd2hlbiB0aGUgdGVudXJlIHBlcmlvZCBpcyBsZXNzIHRoYW4gMTIgbW9udGhzIGFuZCBNb250aGx5IGNoYXJnZXMgYXJlIGFib3ZlIDcwIGRvbGxhcnMqKi4gDQoNCiMgTWFjaGluZSBsZWFybmluZyBtb2RlbHMNCg0KU3BsaXQgdGhlIGRhdGEgaW50byB0aGUgdHJhaW4gKDgwJSkgYW5kIHRlc3Qgc2V0ICgyMCUpLiBBbHNvLCBkZWZpbmUgdGhlIHBhcmFtZXRlcnMgbmVjZXNzYXJ5IGZvciBtb2RlbCB0dW5pbmcgYW5kIGNyb3NzLXZhbGlkYXRpb24uIEJlZm9yZSBJIGp1bXAgdG8gbWFjaGluZSBsZWFybmluZyBJIHdvdWxkIGxpa2UgdG8gc3RhbmRhcmRpemUgdGhlIG51bWVyaWNhbCB2YXJpYWJsZXMgYW5kIHJlbW92ZSBoaWdobHkgY29ycmVsYXRlZCB2YXJpYWJsZXMuIEkgd2lsbCAqKnJlbW92ZSByZWR1bmRhbnQgSW50ZXJuZXQqKiB2YXJpYWJsZXMgdGhhdCBJIGNyZWF0ZWQgZHVyaW5nIGV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXMgZm9yIGJldHRlciB2aXN1YWxpemF0aW9uIGFuZCAqKmxvZyB0cmFuc2Zvcm0gVG90YWxDaGFyZ2VzIGFuZCBub3JtYWxpemUgdGVudXJlIHZhcmlhYmxlcyoqLiANCmBgYHtyIHByZXBhcmVfZGF0YX0NCiMgQ3JlYXRlIG5ldyB2YXJpYWJsZXMNCmNodXJuIDwtIGNodXJuICU+JSBtdXRhdGUoDQogICAgICAgICAgICAgICAgICAgICAgICAgIG1vbnRobHlDaGFyZ2VPdmVyNjkgPSBpZmVsc2UoTW9udGhseUNoYXJnZXMgPiA2OSwiWWVzIiwiTm8iKSAlPiUgYXMuZmFjdG9yKCksDQogICAgICAgICAgICAgICAgICAgICAgICAgIHRlbnVyZUxlc3NUaGFuMjkgPSBpZmVsc2UodGVudXJlIDwgMjksICJZZXMiLCJObyIpICU+JSBhcy5mYWN0b3IoKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgVG90YWxDaGFyZ2VzID0gbG9nKFRvdGFsQ2hhcmdlcykpDQoNCg0KIyBSZW1vdmUgcmVkdW5kYW50IHZhcmlhYmxlcw0KY2h1cm4gPC0gY2h1cm4gJT4lIHNlbGVjdCgtb25lX29mKGMoIkludGVybmV0IikpKQ0KDQojIE5vcm1hbGl6ZSB0aGUgZGF0YSANCnByZV9wcm9jZXNzIDwtIGNodXJuICU+JSBzZWxlY3RfaWYoaXMuZG91YmxlKSAlPiUgcHJlUHJvY2VzcyguLCBtZXRob2QgPSBjKCJjZW50ZXIiLCAic2NhbGUiKSkNCmNodXJuIDwtIHByZWRpY3QocHJlX3Byb2Nlc3MsIG5ld2RhdGEgPSBjaHVybikNCg0KIyBzcGxpdCB0aGUgZGF0YQ0Kc2V0LnNlZWQoMzAwMCkNCmluVHJhaW4gPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihjaHVybiRDaHVybiwgcCA9IDAuOCwgbGlzdCA9IEZBTFNFKQ0KDQp0cmFpbmluZzwtIGNodXJuW2luVHJhaW4sXQ0KdGVzdGluZzwtIGNodXJuWy1pblRyYWluLF0NCg0KIyBQcmludCB0aGUgZGltZW5zaW9ucyBvZiB0cmFpbiBhbmQgdGVzdCBzZXQNCmRpbWVuc2lvbnMgPC0gZGF0YS5mcmFtZShtYXRyaXgoYyhkaW0odHJhaW5pbmcpLCBkaW0odGVzdGluZykpLCBuY29sID0gMiwgYnlyb3cgPSBUUlVFKSkNCmNvbG5hbWVzKGRpbWVuc2lvbnMpIDwtIGMoIlJvd3MiLCAiQ29sdW1ucyIpDQpyb3duYW1lcyhkaW1lbnNpb25zKSA8LSBjKCJUcmFpbiIsICJUZXN0IikNCg0KZGltZW5zaW9ucyAlPiUga2FibGUoKSAlPiUgDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJjb25kZW5zZWQiLCJyZXNwb25zaXZlIiksIGZ1bGxfd2lkdGggPSBGLCBwb3NpdGlvbiA9ICJsZWZ0IiwgZm9udF9zaXplID0gMTApDQoNCnRyYWluX3kgPC0gdHJhaW5pbmcgJT4lIHB1bGwoIkNodXJuIikNCnRyYWluX3ggPC0gdHJhaW5pbmcgJT4lIHNlbGVjdCgtYygiQ2h1cm4iKSkNCg0KdGVzdF95IDwtIHRlc3RpbmcgJT4lIHB1bGwoIkNodXJuIikNCnRlc3RfeCA8LSB0ZXN0aW5nICU+JSBzZWxlY3QoLWMoIkNodXJuIikpDQoNCiMgdGhlIGxvZ2lzdGljIHJlZ3Jlc3Npb24gbW9kZWwgcGFyYW1ldGVycw0KDQptZXRyaWMgPC0gImxvZ0xvc3MiDQoNCmN0cmwgPC0gdHJhaW5Db250cm9sKA0KICBtZXRob2QgPSAiY3YiLCANCiAgbnVtYmVyID0gNSwgDQogIHNhdmVQcmVkaWN0aW9ucyA9ICJhbGwiLCANCiAgY2xhc3NQcm9icyA9IFRSVUUsIA0KICBzdW1tYXJ5RnVuY3Rpb24gPSBtdWx0aUNsYXNzU3VtbWFyeSwgDQogIHZlcmJvc2VJdGVyID0gRkFMU0UpDQpgYGANCg0KIyMgTG9naXN0aWMgUmVncmVzc2lvbg0KDQpgYGB7ciB0aGUgbG9naXN0aWNSZWdyZXNzaW9uLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0Kc2V0LnNlZWQoMzAwMCkNCg0Kd2VpZ2h0cyA8LSBjKDAuNjgxLDEuODgxKQ0KDQptb2RlbF93ZWlnaHRzIDwtIGlmZWxzZSh0cmFpbl95ID09ICJObyIsIHdlaWdodHNbMV0sIHdlaWdodHNbMl0pDQoNCm1vZGVsX2dsbSA8LSB0cmFpbih4ID0gdHJhaW5feCwgeSA9IHRyYWluX3ksIG1ldGhvZCA9ICJnbG0iLCB0ckNvbnRyb2wgPSBjdHJsLCBtZXRyaWMgPSBtZXRyaWMsIHdlaWdodHMgPSBtb2RlbF93ZWlnaHRzKQ0KDQojIEV2YWx1YXRlIHRoZSBtb2RlbCBvbiB0aGUgdGVzdCBkYXRhDQpjbV9nbG0gPC0gY20obW9kZWwgPSBtb2RlbF9nbG0sIGRhdGEgPSB0ZXN0X3gsIHRhcmdldCA9IHRlc3RfeSkNCg0KcHJpbnQoY21fZ2xtKQ0KYGBgDQoNClRoZSBvdmVyYWxsIGFjY3VyYWN5IG9mIHRoZSBtb2RlbCBpcyAqKn43NS4xJSoqIHdpdGggdGhlIEYxIHNjb3JlIGNsb3NlIHRvICoqfjgxLjIlKiouIEJhbGFuY2VkIEFjY3VyYWN5IGlzICoqNzYuNyUqKi4gTGV0IHVzIHNlZSBpZiB3ZSBjYW4gdHdlYWsgdGhlIHByb2JhYmlsaXR5IGN1dG9mZiB0byBhY2hpZXZlIGJldHRlciBhY2N1cmFjeS4NCg0KYGBge3IgdHdlZWtfbHJ9DQojIEdldCB0aGUgcHJvYmFiaWxpdGlzIG9mIHByZWRpY3Rpb24NCnAxIDwtIHByZWRpY3QobW9kZWxfZ2xtLCB0ZXN0X3gsIHR5cGUgPSAicHJvYiIpDQoNCmN1dG9mZiA8LSByb2NfY3V0b2ZmKG1vZGVsX2dsbSwgZGF0YSA9IHRlc3RfeCwgdGVzdF95KQ0KDQpvcHRfcHJlZF9nbG0gPC0gaWZlbHNlKHAxWywyXSA+PSBjdXRvZmYsICJZZXMiLCJObyIpDQoNCiMgT3B0aW1pemVkIHByZWRpY3Rpb25zDQpjb25mdXNpb25NYXRyaXgoZmFjdG9yKG9wdF9wcmVkX2dsbSksIHRlc3RfeSwgbW9kZSA9ICJwcmVjX3JlY2FsbCIpDQpgYGANCg0KQnkgdHdlYWtpbmcgdGhlIGN1dG9mZiB3ZSBpbmNyZWFzZWQgdGhlIEYxIHNjb3JlIG9mIG91ciBtb2RlbCBmcm9tICoqfjgxLjIlKiogdG8gKip+ODIuMyUqKiAuIE92ZXJhbGwgYmFsYW5jZWQgYWNjdXJhY3kgbWV0cmljIGFsc28gaW1wcm92ZWQgZnJvbSAqKn43Ni43JSoqIHRvICoqfjc3LjQlKiogLiANCg0KIyMjIEZlYXR1cmUgYW5hbHlzaXMNCg0KYGBge3IgZmVhdHVyZV9hbmFseXNpc19scn0NCiMgRmVhdHVyZSBhbmFseXNpcw0KcHJpbnQoc3VtbWFyeShtb2RlbF9nbG0pKQ0KYGBgDQoNClRoZSBtb3N0IGltcG9ydGFudCBmZWF0dXJlcyBhY2NvcmRpbmcgdG8gZ2xtIG1vZGVsIGFyZSBhcmUgKlNlbmlvckNpdGl6ZW4qLCAqTXVsdGlwbGVMaW5lcyosKkludGVybmV0U2VydmljZSosICpTdHJlYW1pbmcgc2VydmljZXMqLCpDb250cmFjdCosICpQYXBlcmxlc3NCaWxsaW5nKiwgKlBheW1lbnRNZXRob2QqIGFuZCAqVG90YWxDaGFyZ2VzKi4gDQoNCiMjIyBMZXQgdXMgcmVtb3ZlIGxlc3MgaW1wb3J0YW50IHZhcmlhYmxlcw0KDQpgYGB7ciB0aGUgbG9naXN0aWNSZWdlc3Npb24yLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KIyBDb3B5IGNodXJuIGRhdGFmcmFtZSB0byBjaHVybjINCmNodXJuMiA8LSBjaHVybg0KDQojIENyZWF0ZSBhIG5ldyBjb2x1bW4gZm9yIGFkZCBvbiBzZXJ2aWNlcyBjb21iaW5lZCBpbnRvIG9uZQ0KYWRkc19vbl9jb2x1bW5zIDwtIGNodXJuMiAlPiUgc2VsZWN0KGMoYWRkc19vblszOjZdKSkgJT4lIG11dGF0ZV9hdCgudmFycyA9IHZhcnMoT25saW5lU2VjdXJpdHk6VGVjaFN1cHBvcnQpLCAuZnVucyA9IGZ1bnMoaWZlbHNlKC4gPT0gIk5vIiwgMCwgMSkpKQ0KDQpjaHVybjIkYWRkc19vbiA8LSByb3dTdW1zKGFkZHNfb25fY29sdW1ucykgDQoNCiMgUmVtb3ZlIHJlZHVuZGFudCB2YXJpYWJsZXMgYW5kIGxlc3MgaW1wb3J0YW50IHZhcmlhYmxlcw0KY2h1cm4yIDwtIGNodXJuMiAlPiUgc2VsZWN0KC1vbmVfb2YoYygiZ2VuZGVyIiwgIlBhcnRuZXIiLCAiRGVwZW5kZW50cyIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgInRlbnVyZSIsIlBob25lU2VydmljZSIgLCJPbmxpbmVTZWN1cml0eSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIk9ubGluZUJhY2t1cCIsICJEZXZpY2VQcm90ZWN0aW9uIiwiVGVjaFN1cHBvcnQiLCAiTW9udGhseUNoYXJnZXMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJjaGFuZ2VJbkJpbGxTaWduIiwgImNoYW5nZUluQmlsbCIsInRlbnVyZUxlc3NUaGFuMjkiKSkpDQoNCiMgTm9ybWFsaXplIHRoZSBkYXRhIA0KcHJlX3Byb2Nlc3MgPC0gY2h1cm4yICU+JSBzZWxlY3QoYWRkc19vbikgJT4lIHByZVByb2Nlc3MoLiwgbWV0aG9kID0gYygiY2VudGVyIiwgInNjYWxlIikpDQpjaHVybjIgPC0gcHJlZGljdChwcmVfcHJvY2VzcywgbmV3ZGF0YSA9IGNodXJuMikNCg0KIyByZWJ1aWxkIHRoZSB0cmFpbiBhbmQgdGVzdCBzZXQgd2l0aCB0aGUgc2FtZSBzcGxpdCBhcyBiZWZvcmUNCg0KdHJhaW5pbmcyPC0gY2h1cm4yW2luVHJhaW4sXQ0KdGVzdGluZzI8LSBjaHVybjJbLWluVHJhaW4sXQ0KDQojIFByaW50IHRoZSBkaW1lbnNpb25zIG9mIHRyYWluIGFuZCB0ZXN0IHNldA0KZGltZW5zaW9ucyA8LSBkYXRhLmZyYW1lKG1hdHJpeChjKGRpbSh0cmFpbmluZzIpLCBkaW0odGVzdGluZzIpKSwgbmNvbCA9IDIsIGJ5cm93ID0gVFJVRSkpDQpjb2xuYW1lcyhkaW1lbnNpb25zKSA8LSBjKCJSb3dzIiwgIkNvbHVtbnMiKQ0Kcm93bmFtZXMoZGltZW5zaW9ucykgPC0gYygiVHJhaW4iLCAiVGVzdCIpDQoNCmRpbWVuc2lvbnMgJT4lIGthYmxlKCkgJT4lIA0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygiY29uZGVuc2VkIiwicmVzcG9uc2l2ZSIpLCBmdWxsX3dpZHRoID0gRiwgcG9zaXRpb24gPSAibGVmdCIsIGZvbnRfc2l6ZSA9IDEwKQ0KDQp0cmFpbl95MiA8LSB0cmFpbmluZzIgJT4lIHB1bGwoIkNodXJuIikNCnRyYWluX3gyIDwtIHRyYWluaW5nMiAlPiUgc2VsZWN0KC1jKCJDaHVybiIpKQ0KDQp0ZXN0X3kyIDwtIHRlc3RpbmcyICU+JSBwdWxsKCJDaHVybiIpDQp0ZXN0X3gyIDwtIHRlc3RpbmcyICU+JSBzZWxlY3QoLWMoIkNodXJuIikpDQoNCiMgdHJhaW4gdGhlIG5ldyBtb2RlbA0Kc2V0LnNlZWQoMzAwMCkNCg0Kd2VpZ2h0cyA8LSBjKDAuNjgxLDEuODgxKQ0KDQptb2RlbF93ZWlnaHRzIDwtIGlmZWxzZSh0cmFpbl95MiA9PSAiTm8iLCB3ZWlnaHRzWzFdLCB3ZWlnaHRzWzJdKQ0KDQptb2RlbF9nbG0gPC0gdHJhaW4oeCA9IHRyYWluX3gyLCB5ID0gdHJhaW5feTIsIG1ldGhvZCA9ICJnbG0iLCB0ckNvbnRyb2wgPSBjdHJsLCBtZXRyaWMgPSBtZXRyaWMsIHdlaWdodHMgPSBtb2RlbF93ZWlnaHRzKQ0KDQojIEV2YWx1YXRlIHRoZSBtb2RlbCBvbiB0aGUgdGVzdCBkYXRhDQpjbV9nbG0gPC0gY20obW9kZWwgPSBtb2RlbF9nbG0sIGRhdGEgPSB0ZXN0X3gyLCB0YXJnZXQgPSB0ZXN0X3kyKQ0KDQpwcmludChjbV9nbG0pDQpgYGANCg0KQnkgcmVtb3ZpbmcgdGhlIHZhcmlhYmxlcyB3ZSBzbGlnaHRseSBkZWNyZWFzZWQgdGhlIHByZWRpY3RpdmUgYWJpbGl0eSBvZiBvdXIgbG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbC4gQmFsYW5jZWQgQWNjdXJhY3kgZGVjcmVhc2VkIGZyb20gKip+NzYuNyUqKiB0byAqKjc2LjMlKiouIEhvd2V2ZXIsIGl0IGlzIGEgbXVjaCBzaW1wbGVyIG1vZGVsIGFuZCB0aHVzIHdvdWxkIGJlIGxlc3MgcHJvbmUgdG8gb3ZlcmZpdHRpbmcuDQpMZXQgdXMgc2VlIGlmIHdlIGNhbiBpbXByb3ZlIHRoZSBtb2RlbCBieSB0d2Vha2luZyB0aGUgY3V0b2ZmLg0KDQpgYGB7ciB0d2Vha19scjJ9DQojIEdldCB0aGUgcHJvYmFiaWxpdGlzIG9mIHByZWRpY3Rpb24NCnAxIDwtIHByZWRpY3QobW9kZWxfZ2xtLCB0ZXN0X3gyLCB0eXBlID0gInByb2IiKQ0KDQpjdXRvZmYgPC0gcm9jX2N1dG9mZihtb2RlbF9nbG0sIGRhdGEgPSB0ZXN0X3gyLCB0ZXN0X3kyKQ0KDQpvcHRfcHJlZF9nbG0gPC0gaWZlbHNlKHAxWywyXSA+PSBjdXRvZmYsICJZZXMiLCJObyIpDQoNCiMgT3B0aW1pemVkIHByZWRpY3Rpb25zDQpjb25mdXNpb25NYXRyaXgoZmFjdG9yKG9wdF9wcmVkX2dsbSksIHRlc3RfeTIsIG1vZGUgPSAicHJlY19yZWNhbGwiKQ0KYGBgDQoNCkFnYWluIGJ5IHR3ZWFraW5nIHRoZSBjdXRvZmYgd2Ugd2VyZSBhYmxlIHRvIGluY3JlYXNlIHRoZSBCYWxhbmNlZCBBY2N1cmFjeSBvZiBvdXIgbW9kZWwgZnJvbSAqKn43Ni4zJSoqIHRvICoqfjc3LjAlKiogLiBIb3dldmVyLCBpdCBpcyBzbGlnaHRseSBsZXNzIHRoYW4gdGhlIHByZXZpb3VzIG1vcmUgY29tcGxleCBsb2dpc3RpYyByZWdyZXNzaW9uIG1vZGVsIHdoZXJlIHRoZSBCYWxhbmNlZCBBY2N1cmFjeSB3YXMgKip+NzcuNCUqKiAuIE5ldmVydGhlbGVzcywgdGhpcyBpcyBhIGJldHRlciBtb2RlbCBiZWNhdXNlIHdlIHVzZWQgZmV3ZXIgdmFyaWFibGVzIGFuZCB0aGUgbW9kZWwgYWNjdXJhY3kgZGlkIG5vdCBkZWNyZWFzZSBzaWduaWZpY2FudGx5LiANCg0KIyMgRGVjaXNpb24gdHJlZXMgDQoNCmBgYHtyIGR0LCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KIyBMZXQgdXMgYnVpbGQgZGVjaXNpb24gdHJlZSBtb2RlbCANCnNldC5zZWVkKDMwMDApDQoNCm1vZGVsX2NhcnQgPC0gdHJhaW4oeCA9IHRyYWluX3gyLCB5ID0gdHJhaW5feTIsIG1ldGhvZCA9ICJycGFydCIsIHRyQ29udHJvbCA9IGN0cmwsIG1ldHJpYyA9IG1ldHJpYywgdHVuZUxlbmd0aCA9IDE1LCB3ZWlnaHRzID0gbW9kZWxfd2VpZ2h0cykNCg0KY21fY2FydCA8LSBjbShtb2RlbF9jYXJ0LHRlc3RfeDIsIHRlc3RfeTIpDQoNCnByaW50KGNtX2NhcnQpDQpgYGANCg0KVGhlICoqQmFsYW5jZWQgQWNjdXJhY3kgb2YgdGhlIGRlY2lzaW9uIHRyZWUgbW9kZWwgaXMgfjc1LjUlKiogbGVzcyB0aGFuIHRoZSBsb2dpc3RpYyByZWdyZXNzaW9uLiBMZXQgdXMgb3B0aW1pemUgaXQuDQoNCmBgYHtyIHR3ZWFrX2R0fQ0KIyBHZXQgdGhlIHByb2JhYmlsaXRpcyBvZiBwcmVkaWN0aW9uDQpwMiA8LSBwcmVkaWN0KG1vZGVsX2NhcnQsIHRlc3RfeDIsIHR5cGUgPSAicHJvYiIpDQoNCmN1dG9mZiA8LSByb2NfY3V0b2ZmKG1vZGVsX2NhcnQsIGRhdGEgPSB0ZXN0X3gyLCB0ZXN0X3kyKQ0KDQpvcHRfcHJlZF9jYXJ0IDwtIGlmZWxzZShwMVssMl0gPj0gY3V0b2ZmLCAiWWVzIiwiTm8iKQ0KDQojIE9wdGltaXplZCBwcmVkaWN0aW9ucw0KY29uZnVzaW9uTWF0cml4KGZhY3RvcihvcHRfcHJlZF9jYXJ0KSwgdGVzdF95MiwgbW9kZSA9ICJwcmVjX3JlY2FsbCIpDQpgYGANClVubGlrZSB0aGUgbG9naXN0aWMgcmVncmVzc2lvbiwgd2Ugd2VyZSBub3QgYWJsZSB0byBpbXByb3ZlIHRoZSBCYWxhbmNlZCBBY2N1cmFjeSBvZiB0aGUgRFQgbW9kZWwgYnkgb3B0aW1pemluZyBpdCBob3dldmVyLCBSZWNhbGwgYW5kIHRoZSBGMSBzY29yZSBpbXByb3ZlZCBkcmFtYXRpY2FsbHkuICAgDQoNCiMjIFJhbmRvbSBmb3Jlc3QNCg0KYGBge3IgcmZ9DQpzZXQuc2VlZCgzMDAwKQ0KbW9kZWxfcmYgPC0gdHJhaW4oeCA9IHRyYWluX3gyLCB5ID0gdHJhaW5feTIsIG1ldGhvZCA9ICJyYW5nZXIiLCB0ckNvbnRyb2wgPSBjdHJsLCBtZXRyaWMgPSBtZXRyaWMsIHR1bmVMZW5ndGggPSAxMSwgd2VpZ2h0cyA9IG1vZGVsX3dlaWdodHMpDQoNCmNtX3JmIDwtIGNtKG1vZGVsX3JmLHRlc3RfeDIsIHRlc3RfeTIpDQoNCnByaW50KGNtX3JmKQ0KYGBgDQoNCg0KVGhlICoqYmFsYW5jZWQgYWNjdXJhY3kgb2YgdGhlIHJhbmRvbSBmb3Jlc3QgdHJlZSBtb2RlbCBpcyB+NzQuNCoqIGxlc3MgdGhhbiB0aGUgbG9naXN0aWMgcmVncmVzc2lvbiBhbmQgZGVjaXNpb24gdHJlZSBtb2RlbC4gTGV0IHVzIG9wdGltaXplIGl0Lg0KDQoNCmBgYHtyIHR3ZWFrX3JmfQ0KIyBHZXQgdGhlIHByb2JhYmlsaXRpcyBvZiBwcmVkaWN0aW9uDQpwMyA8LSBwcmVkaWN0KG1vZGVsX3JmLCB0ZXN0X3gyLCB0eXBlID0gInByb2IiKQ0KDQpjdXRvZmYgPC0gcm9jX2N1dG9mZihtb2RlbF9yZiwgZGF0YSA9IHRlc3RfeDIsIHRlc3RfeTIpDQoNCm9wdF9wcmVkX3JmIDwtIGlmZWxzZShwMVssMl0gPj0gY3V0b2ZmLCAiWWVzIiwiTm8iKQ0KDQojIE9wdGltaXplZCBwcmVkaWN0aW9ucw0KY29uZnVzaW9uTWF0cml4KGZhY3RvcihvcHRfcHJlZF9yZiksIHRlc3RfeTIsIG1vZGUgPSAicHJlY19yZWNhbGwiKQ0KYGBgDQoNCkxpa2UgTFIgYW5kIERUIG1vZGVscyB0aGUgKipiYWxhbmNlZCBhY2N1cmFjeSoqIG9mIHRoZSBSRiBtb2RlbCB3YXMgaW1wcm92ZWQgZnJvbSAqKn43NC40JSoqIHRvICoqfjc1LjklKiogaG93ZXZlciwgUmVjYWxsIGFuZCBGMSBzY29yZSBkZWNyZWFzZWQgc2lnbmlmaWNhbnRseS4gVGhlICoqU3BlY2lmaWNpdHkqKiBvZiB0aGUgbW9kZWwgaXMgdmVyeSBoaWdoICoqfjg0LjUlKiosIGluIGZhY3QsIHRoZSBoaWdoZXN0IHdlIGhhdmUgZ290IHRpbGwgbm93LiBUaGlzIG1lYW5zIHRoYXQgdGhlIFJGIG1vZGVsIGlzIGRvaW5nIHdlbGwgaW4gcHJlZGljdGluZyBjdXN0b21lcnMgd2hvIHdpbGwgY2h1cm4gaG93ZXZlciwgdGhlcmUgYXJlIG1hbnkgRmFsc2UgTmVnYXRpdmVzIGFzIHdlbGwuDQoNCiMjIE11bHRpbGF5ZXIgcGVyY2VwdHJvbiBtb2RlbA0KDQpgYGB7ciBtbHBfZGF0YV9wcmVwfQ0KIyBDcmVhdGUgb25lIGhvdCBlbmNvZGVkIHZhcmlhYmxlcw0KZG15IDwtIGR1bW15VmFycygiflNlbmlvckNpdGl6ZW4gKyBNdWx0aXBsZUxpbmVzICsgSW50ZXJuZXRTZXJ2aWNlICsgU3RyZWFtaW5nVFYgKyBTdHJlYW1pbmdNb3ZpZXMgKyBDb250cmFjdCArIFBhcGVybGVzc0JpbGxpbmcgKyBQYXltZW50TWV0aG9kICsgdGVudXJlX2dyb3VwICsgbW9udGhseUNoYXJnZU92ZXI2OSIsIGRhdGEgPSBjaHVybjIpDQoNCm9oZV9jaHVybiA8LSBwcmVkaWN0KGRteSwgbmV3ZGF0YSA9IGNodXJuMikgJT4lIGFzLmRhdGEuZnJhbWUoKQ0KbnVtX2NodXJuIDwtIGNodXJuMiAlPiUgc2VsZWN0X2lmKGlzLm51bWVyaWMpDQoNCnByZVByb2Nlc3NPYmplY3QgPC0gcHJlUHJvY2VzcyhvaGVfY2h1cm4sIG1ldGhvZCA9IGMoImNlbnRlciIsInNjYWxlIikpDQpvaGVfY2h1cm4gPC0gcHJlZGljdChwcmVQcm9jZXNzT2JqZWN0LCBvaGVfY2h1cm4pDQoNCmNodXJuX2RhdGEgPC0gY2JpbmQob2hlX2NodXJuLCBudW1fY2h1cm4pICU+JSBhcy5tYXRyaXgoKQ0KDQpjaHVybl9kYXRhMiA8LSBjaHVybl9kYXRhWyxjKCJTZW5pb3JDaXRpemVuLlllcyIsICJNdWx0aXBsZUxpbmVzLlllcyIsICJJbnRlcm5ldFNlcnZpY2UuRFNMIiwiSW50ZXJuZXRTZXJ2aWNlLkZpYmVyIG9wdGljIiwgIkludGVybmV0U2VydmljZS5ObyIsICJTdHJlYW1pbmdUVi5ZZXMiLCAiU3RyZWFtaW5nTW92aWVzLlllcyIsIkNvbnRyYWN0Lk1vbnRoLXRvLW1vbnRoIiwiQ29udHJhY3QuT25lIHllYXIiLCJDb250cmFjdC5Ud28geWVhciIsICJQYXBlcmxlc3NCaWxsaW5nLlllcyIsICJQYXltZW50TWV0aG9kLkJhbmsgdHJhbnNmZXIiLCAiUGF5bWVudE1ldGhvZC5DcmVkaXQgY2FyZCIsICJQYXltZW50TWV0aG9kLkVsZWN0cm9uaWMgY2hlY2siLCAiUGF5bWVudE1ldGhvZC5NYWlsZWQgY2hlY2siLCAidGVudXJlX2dyb3VwLj42ME0iLCAidGVudXJlX2dyb3VwLjAtMTJNIiwidGVudXJlX2dyb3VwLjEyLTI0TSIsInRlbnVyZV9ncm91cC4yNC00OE0iLCJ0ZW51cmVfZ3JvdXAuNDgtNjBNIiwgIm1vbnRobHlDaGFyZ2VPdmVyNjkuWWVzIiwgIlRvdGFsQ2hhcmdlcyIsICJhZGRzX29uIildDQoNCnRyYWluX2RhdGEgPC0gY2h1cm5fZGF0YTJbaW5UcmFpbixdDQp0ZXN0X2RhdGEgPC0gY2h1cm5fZGF0YTJbLWluVHJhaW4sXQ0KDQp0cmFpbl9sYWJlbHMgPC0gdG9fY2F0ZWdvcmljYWwoYXMuaW50ZWdlcih0cmFpbl95MiktMSwgbnVtX2NsYXNzZXMgPSAyKQ0KdGVzdF9sYWJlbHMgPC0gdG9fY2F0ZWdvcmljYWwoYXMuaW50ZWdlcih0ZXN0X3kyKS0xLCBudW1fY2xhc3NlcyA9IDIpDQpgYGANCg0KTGV0IHVzIGRvIHRoZSBncmlkIHNlYXJjaCB3aXRoIGRpZmZlcm5ldCBvcHRpbWl6ZXJzLCBhY3RpdmF0b3JzIGFuZCBudW1iZXIgb2YgaGlkZGVuIGxheWVycyBhbmQgY2hvb3NlIHRoZSBiZXN0IG9uZSB0byBidWlsZCBvdXIgZmluYWwgbW9kZWwuDQoNCkJlc3QgbW9kZWwgdXNlcyAqcm1zcHJvcCogb3B0aW1pemVyIHdpdGggKnJlbHUqIGFjdGl2YXRvciBhbmQgKmhpZGRlbiBsYXllciBzdHJ1Y3R1cmUgKDgsNCwyKSouIA0KDQpgYGB7ciBtbHBfbW9kZWwsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQpzZXQuc2VlZCgzMDAwKQ0KY2xhc3Nfd2VpZ2h0cyA9IGxpc3QoJzAnID0gd2VpZ2h0c1sxXSwgJzEnID0gd2VpZ2h0c1syXSkNCg0KIyBDcmVhdGUgYSBmdW5jdGlvbiB0byB0cmFpbiB0aGUgbmV0d29yaw0KdHJhaW5fbmV0d29yayA8LSBmdW5jdGlvbihzdHJ1Y3R1cmUsIGFjdGl2YXRpb24sb3B0aW1pemVyLCBlcG9jaHMpew0KICBzdXBwcmVzc01lc3NhZ2VzKHVzZV9zZXNzaW9uX3dpdGhfc2VlZCgzMDAwKSkNCiAgbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpDQogIA0KICBtb2RlbCAlPiUNCiAgICBsYXllcl9kZW5zZSh1bml0cyA9IHN0cnVjdHVyZVsyXSwgYWN0aXZhdGlvbiA9IGFjdGl2YXRpb24sIGlucHV0X3NoYXBlID0gc3RydWN0dXJlWzFdKSAlPiUNCiAgICBsYXllcl9kcm9wb3V0KHJhdGUgPSAwLjMpICU+JQ0KICAgIGxheWVyX2RlbnNlKHVuaXRzID0gc3RydWN0dXJlWzNdLCBhY3RpdmF0aW9uID0gYWN0aXZhdGlvbikgJT4lDQogICAgbGF5ZXJfZHJvcG91dChyYXRlID0gMC4yKSAlPiUNCiAgICBsYXllcl9kZW5zZSh1bml0cyA9IHN0cnVjdHVyZVs0XSwgYWN0aXZhdGlvbiA9IGFjdGl2YXRpb24pICU+JQ0KICAgIGxheWVyX2Ryb3BvdXQocmF0ZSA9IDAuMSkgJT4lDQogICAgbGF5ZXJfZGVuc2UodW5pdHMgPSAyLCBhY3RpdmF0aW9uID0gInNvZnRtYXgiKQ0KICANCiAgbW9kZWwgJT4lIA0KICAgIGNvbXBpbGUobG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5Iiwgb3B0aW1pemVyID0gb3B0aW1pemVyLCBtZXRyaWMgPSBjKCJhY2N1cmFjeSIpKQ0KDQogIGhpc3RvcnkgPC0gbW9kZWwgJT4lDQogICAgZml0KHggPSB0cmFpbl9kYXRhLCB5ID0gdHJhaW5fbGFiZWxzLCBzaHVmZmxlID0gVCwgZXBvY2hzID0gZXBvY2hzLCBiYXRjaF9zaXplID0gNjAwLCANCiAgICAgICAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMywgY2xhc3Nfd2VpZ2h0ID0gY2xhc3Nfd2VpZ2h0cywgdmVyYm9zZSA9IDApDQoNCiAgaGlzdG9yeV9kZiA8LSBhcy5kYXRhLmZyYW1lKGhpc3RvcnkpDQogIGFjYyA8PC0gaGlzdG9yeV9kZltucm93KGhpc3RvcnlfZGYpLDJdDQoNCiAgcHJlZiA8LSBtb2RlbCAlPiUgZXZhbHVhdGUodGVzdF9kYXRhLCB0ZXN0X2xhYmVscywgdmVyYm9zZSA9IDApDQoNCiAgdGVzdGFjYyA8PC0gcHJlZiRhY2MNCiAgDQogIHJldHVybihtb2RlbCkNCn0NCg0Kc2FtcGxlX2Vwb2NocyA8LSAxMDANCg0KIyBJbml0aWFsaXNlIGVtcHR5IGxpc3RzIHRvIHN0b3JlIHJlc3VsdHMNCnRyYWluX2FjYyA8LSBjKCkNCnRlc3RfYWNjIDwtIGMoKQ0KY29tYmluYXRpb25fdmVjdG9yIDwtIGMoKQ0KDQoNCiMgQ3JlYXRlIGEgdmVjdG9yIGxpc3RpbmcgYWxsIHRoZSBhY3RpdmF0aW9uIGZ1bmN0aW9ucyB3ZSB3aXNoIHRvIHRlc3QNCmFjdGl2YXRpb25fZnVuY3Rpb25zIDwtIGMoImVsdSIsICJoYXJkX3NpZ21vaWQiLCAibGluZWFyIiwgInJlbHUiLCAic2VsdSIsICJzaWdtb2lkIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAic29mdHBsdXMiLCAic29mdHNpZ24iLCAidGFuaCIpDQoNCg0KIyBDcmVhdGUgYSB2ZWN0b3IgbGlzdGluZyBhbGwgdGhlIG9wdGltaXphdGlvbiBmdW5jdGlvbnMgd2Ugd2lzaCB0byB0ZXN0DQpvcHRpbWl6ZXJfZnVuY3Rpb25zIDwtIGMoImFkYWRlbHRhIiwgImFkYWdyYWQiLCAiYWRhbSIsICJhZGFtYXgiLA0KICAgICAgICAgICAgICAgICAgICAgICAgIm5hZGFtIiwgInJtc3Byb3AiLCAic2dkIikNCg0KIyBUdW5lIHRoZSBuZXR3b3JrIHdpdGggZGlmZmVyZW50IHBhcmFtZXRlcnMNCg0KZm9yKGkgaW4gMTpsZW5ndGgob3B0aW1pemVyX2Z1bmN0aW9ucykpew0KICAgIGZvcihqIGluIDE6bGVuZ3RoKGFjdGl2YXRpb25fZnVuY3Rpb25zKSl7DQogICAgICAjIE9wdGltaXplIGhpZGRlbiBsYXllcnMNCiAgICAgIGZvcihrIGluIDI6MTYpDQogICAgICB7DQogICAgICAgIE5OX3N0cnVjdHVyZSA8LSBjKG5jb2wodHJhaW5fZGF0YSksayxhcy5pbnRlZ2VyKGsvMiksYXMuaW50ZWdlcihrLzQpKQ0KICAgICAgICBjb21iaW5hdGlvbiA8LSBwYXN0ZSgiT3B0aW1pemVyOiIsb3B0aW1pemVyX2Z1bmN0aW9uc1tpXSwgIkFjdGl2YXRvcjoiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhY3RpdmF0aW9uX2Z1bmN0aW9uc1tqXSwgIkxheWVyczoiLGssIHNlcCA9ICIgIikNCiAgICAgICAgY29tYmluYXRpb25fdmVjdG9yIDwtIGFwcGVuZChjb21iaW5hdGlvbl92ZWN0b3IsIGNvbWJpbmF0aW9uKQ0KICAgICAgICAjcHJpbnQoY29tYmluYXRpb24pDQoNCiAgICAgICAgIyBDYWxsIHRoZSBmdW5jdGlvbg0KICAgICAgICAgIG1vZGVsIDwtIHRyYWluX25ldHdvcmsoTk5fc3RydWN0dXJlLCBhY3RpdmF0aW9uX2Z1bmN0aW9uc1tqXSwgb3B0aW1pemVyX2Z1bmN0aW9uc1tpXSwgc2FtcGxlX2Vwb2NocykNCiAgICAgICAgICB0cmFpbl9hY2MgPC0gYXBwZW5kKHRyYWluX2FjYywgYWNjKQ0KICAgICAgICAgIHRlc3RfYWNjIDwtIGFwcGVuZCh0ZXN0X2FjYywgdGVzdGFjYykNCiAgICAgIH0NCiAgICB9DQogIH0NCg0KDQojIENvbGxlY3QgdGhlIHJlc3VsdHMNCmNvbWJpbmF0aW9uX21hdHJpeCA8LSBzdHJfc3BsaXQoY29tYmluYXRpb25fdmVjdG9yLCBwYXR0ZXJuID0gIiAiLCBzaW1wbGlmeSA9IFRSVUUpDQoNCnRyYWluX2FjYyA8LSB0cmFpbl9hY2NbIWlzLm5hKHRyYWluX2FjYyldDQp0ZXN0X2FjYyA8LSB0ZXN0X2FjY1shaXMubmEodGVzdF9hY2MpXQ0KDQpyZXN1bHRzIDwtIGRhdGEuZnJhbWUoYWNjX3RyYWluID0gdHJhaW5fYWNjLCBhY2NfdGVzdCA9IHRlc3RfYWNjLCBvcHRpbWl6ZXIgPSBjb21iaW5hdGlvbl9tYXRyaXhbLDJdLCBhY3RpdmF0b3IgPSBjb21iaW5hdGlvbl9tYXRyaXhbLDRdLCBuTGF5ZXJzID0gY29tYmluYXRpb25fbWF0cml4Wyw2XSAsc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFKQ0KDQpyZXN1bHRzICU+JSBncm91cF9ieShvcHRpbWl6ZXIsIGFjdGl2YXRvcikgJT4lIHRvcF9uKC4sIDEsIGFjY190cmFpbikgJT4lIGdncGxvdChhZXMoeCA9IGFjY190cmFpbiwgeSA9IGFjY190cmFpbiwgY29sb3IgPSBvcHRpbWl6ZXIpKSArIGdlb21fcG9pbnQoc2l6ZSA9IDMuNSkgKyBmYWNldF93cmFwKH5hY3RpdmF0b3IpDQoNCg0KIyBGaW5hbCB0cmFpbiB1c2luZyB0aGUgYmVzdCBwYXJhbWV0ZXJzDQpic3RfcGFyYSA8LSBjb21iaW5hdGlvbl92ZWN0b3Jbd2hpY2gubWF4KHJlc3VsdHMkYWNjX3Rlc3QpXQ0KDQpic3RfcGFyYSA8LSBzdHJfc3BsaXQoYnN0X3BhcmEsIHBhdHRlcm4gPSAiICIpICU+JSB1bmxpc3QoKQ0KDQpic3Rfb3B0aW1pemVyIDwtIGJzdF9wYXJhWzJdDQpic3RfYWN0aXZhdG9yIDwtIGJzdF9wYXJhWzRdDQpic3RfbGF5ZXIgPC0gYXMuaW50ZWdlcihic3RfcGFyYVs2XSkNCg0KYnN0X3N0cnVjdHVyZSA8LSBjKG5jb2wodHJhaW5fZGF0YSksYnN0X2xheWVyLGFzLmludGVnZXIoYnN0X2xheWVyLzIpLGFzLmludGVnZXIoYnN0X2xheWVyLzQpKQ0KDQptb2RlbCA8LSB0cmFpbl9uZXR3b3JrKGJzdF9zdHJ1Y3R1cmUsIGJzdF9hY3RpdmF0b3IsIGJzdF9vcHRpbWl6ZXIsIHNhbXBsZV9lcG9jaHMpDQoNCiMgZ2V0IHRoZSBzdW1tYXJ5IG9mIHRoZSBtb2RlbA0Kc3VtbWFyeShtb2RlbCkgJT4lIGthYmxlKCkgDQoNCiMgRXZhbHVhdGUgdGhlIG1vZGVsIG9uIHRlc3Qgc2V0DQptb2RlbCAlPiUgZXZhbHVhdGUodGVzdF9kYXRhLCB0ZXN0X2xhYmVscykNCg0KIyBQcmVkaWN0IHRoZSBjbGFzc2VzIGFuZCBtYWtlIGEgY29uZnVzaW9uIG1hdHJpeA0KDQogcHJlZF9ubiA8LSBtb2RlbCAlPiUgcHJlZGljdF9jbGFzc2VzKHRlc3RfZGF0YSkNCiBwcmVkX25uIDwtIHByZWRfbm4gJT4lIHJlY29kZSgnMCcgPSAiTm8iLCAnMScgPSAiWWVzIikgJT4lIGFzLmZhY3RvcigpDQoNCiBwcmVkX25uX3Byb2IgPC0gbW9kZWwgJT4lIHByZWRpY3RfcHJvYmEodGVzdF9kYXRhKSAlPiUgYXMuZGF0YS5mcmFtZSgpICU+JSBwdWxsKDIpDQoNCiBjb25mdXNpb25NYXRyaXgocHJlZF9ubiwgdGVzdF95MiwgbW9kZSA9ICJwcmVjX3JlY2FsbCIpDQpgYGANCg0KDQoNClRoZSAqKkJhbGFuY2VkIEFjY3VyYWN5Kiogb2YgdGhlIE11bHRpbGF5ZXIgUGVyY2VwdHJvbiBtb2RlbCBpcyAqKjc1LjIlKiogd2hpY2ggaXMgbGVzcyB0aGFuIHRoZSBsb2dpc3RpYyByZWdyZXNzaW9uIG1vZGVsIGhvd2V2ZXIgaXQgaGFzIGEgbXVjaCBiZXR0ZXIgdGhlIEYxIHNjb3JlICoqfjg1LjAlKiphbmQgUmVjYWxsIHNjb3JlICoqfjgyLjYlKiouIFRoaXMgaXMgdGhlIGhpZ2hlc3QgRjEgc2NvcmUgd2UgaGF2ZSBnb3QgdGlsbCBub3cuICANCg0KIyMgR3JhZGllbnQgQm9vc3RpbmcgTWFjaGluZSAoR0JNKQ0KDQpgYGB7ciBnYm0sIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpzZXQuc2VlZCgzMDAwKQ0KY2wgPC0gbWFrZUNsdXN0ZXIoNikNCnJlZ2lzdGVyRG9QYXJhbGxlbChjbCkNCg0KZ2JtR3JpZCA8LSBleHBhbmQuZ3JpZChpbnRlcmFjdGlvbi5kZXB0aCA9IHNlcSg1LDEwLDIpLCBuLnRyZWVzID0gYyg1OjEwKSAqIDUwLCBzaHJpbmthZ2UgPSBzZXEoMC4wMSwgMC4wNSwgMC4wMSksIG4ubWlub2JzaW5ub2RlID0gc2VxKDEwLDQwLDEwKSkNCg0KbW9kZWxfZ2JtIDwtIHRyYWluKHggPSB0cmFpbl9kYXRhLCB5ID0gdHJhaW5feTIsIG1ldGhvZCA9ICJnYm0iLCB0ckNvbnRyb2wgPSBjdHJsLCBtZXRyaWMgPSBtZXRyaWMsIHR1bmVHcmlkID0gZ2JtR3JpZCwgd2VpZ2h0cyA9IG1vZGVsX3dlaWdodHMpDQoNCg0KY21fZ2JtIDwtIGNtKG1vZGVsX2dibSx0ZXN0X2RhdGEsIHRlc3RfeTIpDQoNCnByaW50KGNtX2dibSkNCg0Kc3RvcENsdXN0ZXIoY2wpDQojcHJpbnQoIkNsdXN0ZXIgc3RvcHBlZCIpDQoNCiMgaW5zZXJ0IHNlcmlhbCBiYWNrZW5kLCBvdGhlcndpc2UgZXJyb3IgaW4gc3VtbWFyeS5jb25uZWN0aW9uKGNvbm5lY3Rpb24pIDogaW52YWxpZCBjb25uZWN0aW9uDQpyZWdpc3RlckRvU0VRKCkNCg0KYGBgDQoNClRoZSAqKkJhbGFuY2VkIEFjY3VyYWN5Kiogb2YgdGhlIEdCTSBtb2RlbCBpcyAqKn43Ni4zJSoqIGxlc3MgdGhhbiB0aGUgYmVzdCB3ZSBoYXZlIGluIHRoZSBsb2dpc3RpYyByZWdyZXNzaW9uIG1vZGVsICoqfjc3LjQlKiouDQoNCmBgYHtyIHR3ZWFrX2dibX0NCiMgR2V0IHRoZSBwcm9iYWJpbGl0aXMgb2YgcHJlZGljdGlvbg0KcDQgPC0gcHJlZGljdChtb2RlbF9nYm0sIHRlc3RfZGF0YSwgdHlwZSA9ICJwcm9iIikNCg0KY3V0b2ZmIDwtIHJvY19jdXRvZmYobW9kZWxfZ2JtLCBkYXRhID0gdGVzdF9kYXRhLCB0ZXN0X3kyKQ0KDQpvcHRfcHJlZF9nYm0gPC0gaWZlbHNlKHA0WywyXSA+PSBjdXRvZmYsICJZZXMiLCJObyIpDQoNCiMgT3B0aW1pemVkIHByZWRpY3Rpb25zDQpjb25mdXNpb25NYXRyaXgoZmFjdG9yKG9wdF9wcmVkX2dibSksIHRlc3RfeTIsIG1vZGUgPSAicHJlY19yZWNhbGwiKQ0KYGBgDQoNCkJ5IHR3ZWFraW5nIGN1dG9mZiB2YWx1ZSB3ZSB3ZXJlIGFibGUgdG8gc2xpZ2h0bHkgaW1wcm92ZSB0aGUgKipCYWxhbmNlZCBBY2N1cmFjeSoqIG9mIHRoZSBHQk0gbW9kZWwgZnJvbSAqKn43Ni4zJSoqIHRvICAgKip+NzYuNiUqKiB3aGljaCBpcyBzdGlsbCBsZXNzIHRoYW4gYmVzdCB3ZSBoYXZlIHNvIGZhci4NCg0KIyMgTW9kZWwgQXZlcmFnZWQgTmV1cmFsIE5ldHdvcmsgKEF2Tk5FVCkgDQoNCmBgYHtyIGF2Tk5ldH0NCnNldC5zZWVkKDMwMDApDQpjbCA8LSBtYWtlQ2x1c3Rlcig2KQ0KcmVnaXN0ZXJEb1BhcmFsbGVsKGNsKQ0KDQpubkdyaWQgPC0gZXhwYW5kLmdyaWQoc2l6ZSA9IHNlcSgxLDMsMSksIGRlY2F5ID0gc2VxKDAuMDAwMSwgMC4wMDEsIDAuMDAwMSksIGJhZyA9IGMoVFJVRSwgRkFMU0UpKQ0KDQptb2RlbF9hdk5OZXQgPC0gdHJhaW4oeCA9IHRyYWluX3gyLCB5ID0gdHJhaW5feTIsIG1ldGhvZCA9ICJhdk5OZXQiLCB0ckNvbnRyb2wgPSBjdHJsLCBtZXRyaWMgPSBtZXRyaWMsIHR1bmVHcmlkID0gbm5HcmlkLCB3ZWlnaHRzID0gbW9kZWxfd2VpZ2h0cywgcmVwZWF0cyA9IDEwKQ0KDQoNCmNtX2F2Tk5ldCA8LSBjbShtb2RlbF9hdk5OZXQsdGVzdF94MiwgdGVzdF95MikNCg0KcHJpbnQoY21fYXZOTmV0KQ0KDQpzdG9wQ2x1c3RlcihjbCkNCiNwcmludCgiQ2x1c3RlciBzdG9wcGVkIikNCg0KIyBpbnNlcnQgc2VyaWFsIGJhY2tlbmQsIG90aGVyd2lzZSBlcnJvciBpbiBzdW1tYXJ5LmNvbm5lY3Rpb24oY29ubmVjdGlvbikgOiBpbnZhbGlkIGNvbm5lY3Rpb24NCnJlZ2lzdGVyRG9TRVEoKQ0KYGBgDQoNClRoZSAqYXZOTmV0KiBpcyBhIG1vZGVsIHdoZXJlIHRoZSBzYW1lIG5ldXJhbCBuZXR3b3JrIG1vZGVsIGlzIGZpdCB1c2luZyBkaWZmZXJlbnQgcmFuZG9tIG51bWJlciBzZWVkcy4gQWxsIHRoZSByZXN1bHRpbmcgbW9kZWxzIGFyZSB1c2VkIGZvciBwcmVkaWN0aW9uLiBUaGUgYXZlcmFnZSBOZXVyYWwgTmV0d29yayBoYXMgdGhlIGJlc3QgRjEgc2NvcmUgdGlsbCBub3cgKip+ODcuMCUqKiBldmVuIGJldHRlciB0aGFuIHRoZSBLZXJhcyBtb2RlbCBvZiAqKn44NS4wJSoqIC4gSG93ZXZlciwgdGhlIEJhbGFuY2VkIEFjY3VyYWN5IGhhcyB0YWtlbiBhIGJlYXRpbmcgKip+NjkuNyUqKiAuIExldCB1cyB0cnkgdG8gb3B0aW1pemUgaXQuDQoNCmBgYHtyIHR3ZWFrX2F2Tk5ldH0NCiMgR2V0IHRoZSBwcm9iYWJpbGl0aXMgb2YgcHJlZGljdGlvbg0KcDUgPC0gcHJlZGljdChtb2RlbF9hdk5OZXQsIHRlc3RfeDIsIHR5cGUgPSAicHJvYiIpDQoNCmN1dG9mZiA8LSByb2NfY3V0b2ZmKG1vZGVsX2F2Tk5ldCwgZGF0YSA9IHRlc3RfeDIsIHRlc3RfeTIpDQoNCm9wdF9wcmVkX2F2Tk5ldCA8LSBpZmVsc2UocDVbLDJdID49IGN1dG9mZiwgIlllcyIsIk5vIikNCg0KIyBPcHRpbWl6ZWQgcHJlZGljdGlvbnMNCmNvbmZ1c2lvbk1hdHJpeChmYWN0b3Iob3B0X3ByZWRfYXZOTmV0KSwgdGVzdF95MiwgbW9kZSA9ICJwcmVjX3JlY2FsbCIpDQpgYGANCg0KQnkgdHdlYWtpbmcgdGhlIGN1dG9mZiB3ZSB3ZXJlIGFibGUgdG8gaW1wcm92ZSB0aGUgQmFsYW5jZWQgQWNjdXJhY3kgZnJvbSAqKn42OS43JSoqIHRvICoqfjc3LjMlKiogaG93ZXZlciBpdCBjYW1lIGF0IGFuIGV4cGVuc2Ugb2YgdGhlIEYxIHNjb3JlLiBBbm90aGVyIG5vdGFibGUgY2hhbmdlIHdhcyBpbiB0aGUgKipTcGVjaWZpY2l0eSoqIG9mIHRoZSBtb2RlbCB3aGljaCBpbmNyZWFzZWQgZnJvbSAqKn40OC4wJSoqIHRvICoqfjgxLjUlKiouIA0KDQojIENvbmNsdXNpb24NCg0KVGhlcmUgYXJlIGEgd2lkZSB2YXJpZXR5IG9mIG1hY2hpbmUgbGVhcm5pbmcgYWxnb3JpdGhtcyBhcHBsaWNhYmxlIHRvIHByZWRpY3RpbmcgY3VzdG9tZXIgY2h1cm4uICoqQ2hvb3Npbmcgb25lIG1vZGVsIG92ZXIgdGhlIG90aGVyIHdpbGwgZGVwZW5kIG9uIHRoZSBxdWVzdGlvbnMgd2UgYXJlIHRyeWluZyB0byBhbnN3ZXIqKi4gRm9yIGV4YW1wbGUsIGJ1c2luZXNzZXMgbW9yZSBpbnRlcmVzdGVkIGluIHByZWRpY3RpbmcgY3VzdG9tZXJzIHdobyB3aWxsIGNodXJuIHdpbGwgbGlrZWx5IGNob29zZSBhbiBvcHRpbWl6ZWQgUkYgb3IgYXZOTmV0IG1vZGVsLCBvbiB0aGUgb3RoZXIgaGFuZCBidXNpbmVzc2VzIHdobyB3YW50IHRvIGRlc2lnbiB0aGVpciBzdHJhdGVneSB0byBhbGlnbiBmb3IgY3VzdG9tZXJzIHdobyBhcmUgbGlrZWx5IHRvIHN0YXkgbWF5IGNob29zZSB1bm9wdGltaXplZCBhdk5OZXQgbW9kZWwuIFlldCBvdGhlcnMgbWF5IGNob29zZSBhIExvZ2lzdGljIFJlZ3Jlc3Npb24gbW9kZWwgdG8gcHJlZGljdCBib3RoIHR5cGVzIG9mIGN1c3RvbWVycyBlcXVhbGx5IHdlbGwuIFRvIGNvbmNsdWRlLCB0aGVyZSBpcyBubyBiZXN0IG1vZGVsIGl0IGp1c3QgZGVwZW5kcyBvbiB3aGF0IHlvdSBhcmUgdHJ5aW5nIHRvIGFuc3dlci4NCg0K