Loading in the data

gps <- read.csv("googleplaystore.csv")

Data Cleaning

The following needs to be removed/ cleaned:
1) Record of an App by name Life made wi-fi….
2) The Current Version of the App and Android Version have to be processed to display the current sequence (12.1.2 is 12)
3) The Special characters in Installs(+), Price($), Size(MB) should be removed
4) All the redundant records should be removed
5) The levels in Installs should be fixed to a Descending order
6) Geners should be removed, it is the same as Category and makes it redundant
7) All the other Nas should be removed
8) Fixing Content Rating by combining like levels

gps <- gps[gps$App !=   "Life Made WI-Fi Touchscreen Photo Frame",] %>% na.omit() %>% unique() %>% select(-Genres)
gps$Current.Ver <- gps$Current.Ver %>% as.character()
i <- 1
for(i in c(seq(1, 8892, by = 1))){
  a <- gps$Current.Ver[i] 
  b <- b <- unlist(strsplit(a, "[.]"))
  b <- b[1]
  gps$Current.Ver[i] = b
  rm(b)
  rm(a)
  i <- i + 1
}
rm(i)
gps$Android.Ver <- gps$Android.Ver %>% as.character()
i <- 1
for(i in c(seq(1, 8892, by = 1))){
  a <- gps$Android.Ver[i]
  b <- b <- unlist(strsplit(a, "[.]"))
  b <- b[1]
  gps$Android.Ver[i] <- b
  rm(b)
  rm(a)
  i <- i + 1
}
rm(i)

gps$Category <- gps$Category %>% droplevels()
gps$Installs = gsub("[^0-9.]", '', (gps$Installs %>% as.character()))
    gps$Installs = as.numeric(gps$Installs)
gps$Size = gsub("[^0-9.]", '', (gps$Size %>% as.character()))
    gps$Size = as.numeric(gps$Size)
gps$Price = gsub("[^0-9.]", '', (gps$Price %>% as.character()))
    gps$Price = as.numeric(gps$Price)    

gps$Installs <- factor(gps$Installs, levels = c("1000000000", "500000000", "100000000", "50000000", "10000000", "5000000", "1000000", "500000", "100000", "50000", "10000", "5000", "1000", "500", "100", "50", "10", "5", "1"))

gps$Content.Rating <- gps$Content.Rating %>% as.character() %>% word(1) %>% as.factor() %>% droplevels()

Different Categories and their presence in the market

Of the different kinds of people present in this word, Which category is globally accepted?
What are the top categories that perform the best?

gps_cb <- gps %>% na.omit() %>% group_by(Category) %>% summarize(Sum = sum(Rating), count = n(), `Normalized Rating` = sum(Rating)/n()) %>% arrange(desc(`Normalized Rating`))
kable(head(gps_cb, 7), caption = "Top 7 categories based on Normalized Rating")

Category Sum count Normalized Rating
EVENTS 170.2 38 4.478947
EDUCATION 416.1 95 4.380000
ART_AND_DESIGN 257.3 59 4.361017
PARENTING 191.3 44 4.347727
PERSONALIZATION 1206.6 279 4.324731
BOOKS_AND_REFERENCE 618.0 143 4.321678
BEAUTY 158.8 37 4.291892

kable(tail(gps_cb, 7), caption = "Bottom 7 categories based on Normalized Rating")

Category Sum count Normalized Rating
COMMUNICATION 844.1 206 4.097573
LIFESTYLE 1117.4 273 4.093040
TRAVEL_AND_LOCAL 591.9 147 4.026531
VIDEO_PLAYERS 467.0 116 4.025862
MAPS_AND_NAVIGATION 381.3 95 4.013684
TOOLS 2543.0 634 4.011041
DATING 558.3 141 3.959574

  
gps %>% filter(Category %in% (gps_cb$Category %>% head(15))) %>% ggplot(aes(x = Category, y = Rating, fill = Category)) + geom_violin(color = "black") + theme(legend.position = "none") + geom_hline(size = 0.6, color = "red", linetype = 'dashed', alpha = 0.5, yintercept = mean(gps$Rating %>% na.omit()))+coord_flip()+labs(y = "Rating", x = "Category", title = "Category Vs Rating Violin Plot")

rm(gps_cb)

Events, Education and Personalization are the best in overall rating.
While Dating, Tools, Maps and Navigation are the worst rated app categories.

Subtle and Significant Category Forensics

Which category doesn’t perform well on a overall scale?
What are the total number of installs for different categories?

data <- gps %>% group_by(Category) %>% summarise(Count = n(), `Total installs` = sum(as.numeric(as.character(Installs))), `Normalized Installations` = `Total installs`/Count) 

data %>% 
  ggplot(aes(x = reorder(Category, Count), fill = Category, y = Count))+geom_bar(stat = "identity", color = "black")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Number of Apps present", x = "Category", title = "Number of Apps present in Different Category")


data %>% 
  ggplot(aes(x = reorder(Category, `Total installs`), fill = Category, y = `Total installs`))+geom_bar(stat = "identity", color = "black")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Total number of installs", x = "Category", title = "Total Number of Installations per Category")


data %>% 
  ggplot(aes(x = reorder(Category, `Normalized Installations`), fill = Category, y = `Normalized Installations`))+geom_bar(stat = "identity", color = "black")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Total number of normalized installations", x = "Category", title = "Total Number of Normalized Installations per Category")


info <- cbind((data %>% arrange(desc(data$Count)) %>% select(Category)),
(data %>% arrange(desc(data$`Total installs`)) %>% select(Category)),
(data %>% arrange(desc(data$`Normalized Installations`)) %>% select(Category)))
names(info) <- c("By Count", "By Total Installs", "By Total Normalized Installs")
kable(head(info, 7), caption = "Top 7 categories")

By Count By Total Installs By Total Normalized Installs
FAMILY GAME COMMUNICATION
GAME COMMUNICATION SOCIAL
TOOLS SOCIAL VIDEO_PLAYERS
PRODUCTIVITY PRODUCTIVITY PRODUCTIVITY
FINANCE TOOLS PHOTOGRAPHY
PERSONALIZATION FAMILY TRAVEL_AND_LOCAL
COMMUNICATION PHOTOGRAPHY GAME

kable(tail(info, 7), caption = "Bottom 7 categories")

By Count By Total Installs By Total Normalized Installs
27 HOUSE_AND_HOME LIBRARIES_AND_DEMO COMICS
28 LIBRARIES_AND_DEMO COMICS LIBRARIES_AND_DEMO
29 ART_AND_DESIGN AUTO_AND_VEHICLES AUTO_AND_VEHICLES
30 COMICS MEDICAL BEAUTY
31 PARENTING PARENTING PARENTING
32 EVENTS BEAUTY EVENTS
33 BEAUTY EVENTS MEDICAL

rm(info)

Family has the highest competition (number of apps) followed by tools which don’t seem to be rewarding.
Game, and Productivity do well wrt to number of installs even though they have a lot of competition.
Communication and Social are two domains where there is a surge in downloads and the number of competitors are also less.

Comments, Parenting, and Events can be seen as domains that one must ignore as there are very few downloads and so is the competition. Or they can also be seen as domains that have huge potential and are underdog markets that can be exploited with a disruptive idea.

Content Type

What is the distribution of apps of various content?
Is there an effect of content type of installs?
How are they priced?

gps %>% group_by(Content.Rating) %>% summarise(count = n()) %>%
  ggplot(aes(x = reorder(Content.Rating, -count), y = count, fill = Content.Rating))+ geom_bar(stat = "identity", color = "black")+
 scale_fill_viridis(discrete = TRUE) +
  labs(y = "Count", x = "Content Type", title = "Content type and their presence")

Apps for everyone are present in bulk

Installs vs Rating per content type

gps %>% 
  ggplot(aes(x = Rating, y = as.integer(as.character(gps$Installs)), color = Content.Rating))+
  geom_point()+
  facet_wrap(~Content.Rating) + geom_vline(xintercept = 5 ,colour = 'red', linetype = 'dashed') + geom_vline(xintercept = 4 ,colour = 'red', linetype = 'dashed') +  geom_hline(yintercept = 1001000000 ,colour = 'red', linetype = 'dashed') + geom_hline(yintercept = 751000000 ,colour = 'red', linetype = 'dashed')

Everyone and Teens content type attract the most number of installs and a decent rating.

Rating analysis

App ratings (on a scale of 1 to 5) impact the discoverability, and indicates the company’s overall brand image. Ratings are a key performance indicator of an app.

gps  %>% ggplot(aes(x = Rating))+geom_histogram(binwidth = 0.1, fill = "purple")+geom_vline(color = "red", linetype = 'dashed', xintercept = mean(gps$Rating %>% na.omit()))+theme(legend.position = "none")+labs(y = "Count", x = "Rating", title = "Rating Histogram")

Most of the apps have an average rating above 4 due to which there is a significant skewness present in the rating part.
Ratings are right skewed with major apps being highly rated and only few with a low rating.

This indicates that most of the apps in the Market are sized below 25mb.

The applications with large size can make it difficult for users to download. Higher download durations could upset the user and ignore the app.
More importantly the memory a user has in the device is limited. This is to be tested.
Let’s investigate the same with the data.

The distribution of size

gps %>% ggplot(aes(x = Size))+geom_histogram(binwidth = 1, fill = "purple")+theme(legend.position = "none")+labs(y = "Count", x = "Size (MB)", title = "Size distribution Histogram")


gps  %>% filter(Size < 120) %>% ggplot(aes(x = Size))+geom_histogram(binwidth = 1, fill = "purple")+theme(legend.position = "none")+labs(y = "Count", x = "Size (MB)", title = "Size distribution Histogram (Sizes below 120Mb)")

Most of the apps in the Market are sized below 25mb.

Next is the size.
The applications with large size can make it difficult for users to download. Higher download durations could upset the user and ignore the app.
More importantly the memory a user has in the device is limited. This is to be tested.

Impact of Size on Rating

p <- gps %>% ggplot(aes(x = Size, y = Rating))+geom_point()+labs(y = "Rating", x = "Size (MB)", title = "Size Vs Rating") 
ggMarginal(p, type = "histogram")



p <- gps %>% filter(Size < 100) %>% ggplot(aes(x = Size, y = Rating))+geom_point()+labs(y = "Rating", x = "Size (MB)", title = "Size Vs Rating (Sizes below 100)") 
ggMarginal(p, type = "histogram")




p <- gps %>% filter(Size < 10) %>% ggplot(aes(x = Size, y = Rating))+geom_point()+labs(y = "Rating", x = "Size (MB)", title = "Size Vs Rating (Sizes below 10)")  
ggMarginal(p, type = "histogram")

rm(p)

Majority of apps with high rating (rating over 4) are between 1 MB to 10 MB.

What is the variation in the number of insallations

gps %>% ggplot(aes(x = Installs)) +
  geom_histogram(stat = 'count', bins = 100, fill = "purple", color = 'black')+
coord_flip() +  scale_y_continuous(breaks = seq(0, 1500, 100))+labs(y = "Count", x = "Installs Range", title = "Installs distribution Histogram")

The Installs are perfectly distributed as expected. The high end market is very competitive and very few apps fall into that space, while most of the others fall behind the race getting in mediocre downloads. But the interesting part is that there are a lot of apps that fall into 1M downloads point. This is interesting as this gives a hope to a lot of designers as there is scope.

data <- gps %>% group_by(Installs) %>% summarise(Count = n()) %>% arrange(desc(Installs)) 
data <- data %>% mutate(product = as.numeric(as.character(data$Installs))*data$Count, `market share` = product/146625951338*100, `market share per app in the segment` = `market share`/Count)
names(data) <- c("Installs Range", "Total Number of apps present", "Total Installations","Market share of the segment", "Market share per app in that segment")

kable(data, caption = "Installations and their contribution")
Installs Range Total Number of apps present Total Installations Market share of the segment Market share per app in that segment
1 3 3 0.0000000 0.0000000
5 9 45 0.0000000 0.0000000
10 69 690 0.0000005 0.0000000
50 56 2800 0.0000019 0.0000000
100 303 30300 0.0000207 0.0000001
500 199 99500 0.0000679 0.0000003
1000 698 698000 0.0004760 0.0000007
5000 426 2130000 0.0014527 0.0000034
10000 989 9890000 0.0067451 0.0000068
50000 462 23100000 0.0157544 0.0000341
100000 1110 111000000 0.0757028 0.0000682
500000 516 258000000 0.1759579 0.0003410
1000000 1486 1486000000 1.0134632 0.0006820
5000000 683 3415000000 2.3290556 0.0034100
10000000 1132 11320000000 7.7203250 0.0068201
50000000 272 13600000000 9.2753021 0.0341004
100000000 369 36900000000 25.1660771 0.0682008
500000000 61 30500000000 20.8012291 0.3410038
1000000000 49 49000000000 33.4183680 0.6820075

The top five segments make up 96.38% of the market share. This implies that an app present in the top-most installion segment make up close 0.7% of market.

Impact of Price Does Price have anything to do with the number of installs?

gps %>% ggplot(aes(x = Type, y = as.numeric(as.character(gps$Installs)), fill = Type))+geom_boxplot()+scale_fill_manual(values=c('#E69F00', '#56B4E9'))+labs(y = "Installs", x = "Free or Paid", title = "Influence of type of availability on Installations")


#This is because of the outliers present at a very high value. This can be fixed by using the log transformation.

gps  %>% 
  ggplot(aes(x = Type, y = log(as.numeric(as.character(gps$Installs))), fill = Type))+geom_boxplot()+scale_fill_manual(values=c('#E69F00', '#56B4E9'))+labs(y = "Installs after log transformation", x = "Free or Paid", title = "Influence of type of availability on Installations")

NA

As expected, free apps have higher installations than paid.

Price Vs Category

gps %>% 
  ggplot(aes(x = Price, y = Category, color = Category))+
  geom_jitter() + theme(legend.position = "none")+labs(y = "Category", x = "Price", title = "Price Vs Category")


gps %>% select(c("App", "Price")) %>% arrange(desc(Price)) %>% head(20)

The top 15 most expensive apps appear to be junk.

Ignoring them and proceeding with the analysis gives

gps %>% filter(Price < 80) %>%
  ggplot(aes(x = Price, y = Category, color = Category))+
  geom_jitter() + theme(legend.position = "none")+labs(y = "Category", x = "Price", title = "Price Vs Category after cleaning junk")

The most expensive apps belong to Medical and Lifestyle followed by Family.

TEXT MINING - Sentimental Analysis

Word Cloud Function


  wordcloud_c <- function(data, num_words = 100, background = "white") {
  
  # If text is provided, convert it to a dataframe of word frequencies
  if (is.character(data)) {
    corpus <- Corpus(VectorSource(data))
    corpus <- tm_map(corpus, tolower)
    corpus <- tm_map(corpus, removePunctuation)
    corpus <- tm_map(corpus, removeNumbers)
    corpus <- tm_map(corpus, removeWords, stopwords("english"))
    tdm <- as.matrix(TermDocumentMatrix(corpus))
    data <- sort(rowSums(tdm), decreasing = TRUE)
    data <- data.frame(word = names(data), freq = as.numeric(data))
  }
  
  # Make sure a proper num_words is provided
  if (!is.numeric(num_words) || num_words < 3) {
    num_words <- 3
  }  
  
  # Grab the top n most common words
  data <- head(data, n = num_words)
  if (nrow(data) == 0) {
    return(NULL)
  }
  
  wordcloud2(data, backgroundColor = background)
}

SA - Review Analysis

The reviews had some special characters, which had to be removed.
This data is grouped with the data used for EDA and all the irrelevant attributes are dropped.
The Sentiment polarity and Sentiment Subjectivity were then grouped with Category to see how they impact it.

gspr <- read.csv("googleplaystore_user_reviews.csv") %>% na.omit()
gspr$App <- as.character(gspr$App)
gspr$Translated_Review <- as.character(gspr$Translated_Review)
data <- gps %>% select(-c("Reviews", "Price", "Last.Updated", "Android.Ver")) %>% as.data.frame()
data$App <- as.character(data$App)
data <- data %>% inner_join(gspr, by = "App")
#gspr$Languages <- textcat(gspr$Translated_Review)
#gspr <- gspr %>% filter(Languages == "english")
#wordcloud_c(gspr[,2], 5)
#get_sentiments(gspr[,2])
#All the sentiment extractions has already been done in the data so proceding to crunch in that info

data$Sentiment_Polarity <- round(data$Sentiment_Polarity, 2)
data$Sentiment_Subjectivity <- round(data$Sentiment_Subjectivity, 2)

Word Clouds for Positive, Neutral and Negative Reviews

data_po <- data %>% filter(Sentiment_Polarity == 1) %>% select("Translated_Review") 
data_nu <- data %>% filter(Sentiment_Polarity == 0) %>% select("Translated_Review")
data_ne <- data %>% filter(Sentiment_Polarity == -1) %>% select("Translated_Review")
wordcloud_c(data_po[,1], 100)

wordcloud_c(data_nu[,1], 100)

wordcloud_c(data_ne[,1], 100)


rm(data_ne)
rm(data_nu)
rm(data_po)

SA - Text Forensics

das <- data %>% select(-"Translated_Review")
das <- das %>% group_by(Category, Sentiment) %>% summarise(Count = n(), NP = round(sum(Sentiment_Polarity)/n(),2))
das <- das %>% mutate(s = Sentiment)
das <- das %>% spread(Sentiment, NP)
names(das) <-  c("Category", "Count", "s", "Ne_S", "Nu_S", "Po_S")
das <- das %>% spread(s, Count)
names(das) <-  c("Category", "Ne_S", "Nu_S", "Po_S", "Ne_C", "Nu_C", "Po_C")
das[is.na(das)] <- 0
das <- das %>% group_by(Category) %>% summarise(Nu_C = sum(Nu_C), Ne_C = sum(Ne_C), Po_C = sum(Po_C), Nu_S = sum(Nu_S), Ne_S = sum(Ne_S), Po_S = sum(Po_S))


das %>% ggplot(aes(y = Po_S, x = reorder(Category, Po_S), fill = Category))+geom_bar(stat = "identity")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Sentiment Polarity", x = "Category", title = "Strength of Positive Sentiment Vs Category")


das %>% ggplot(aes(y = Po_C, x = reorder(Category, Po_C), fill = Category))+geom_bar(stat = "identity")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Number of Positive Reviews", x = "Category", title = "Number of Positive Vs Category")


das %>% ggplot(aes(y = Ne_S, x = reorder(Category, -Ne_S), fill = Category))+geom_bar(stat = "identity")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Sentiment Polarity", x = "Category", title = "Strength of Negative Sentiment Vs Category")


das %>% ggplot(aes(y = Ne_C, x = reorder(Category, Ne_C), fill = Category))+geom_bar(stat = "identity")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Number of Negative Reviews", x = "Category", title = "Number of Negative Vs Category")



das %>% ggplot(aes(y = (Po_C-Ne_C)/(Ne_C), x = reorder(Category, ((Po_C-Ne_C)/(Ne_C))), fill = Category))+geom_bar(stat = "identity")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Percentage Number of Positive Reviews wrt Negative Reviews", x = "Category", title = "Percentage of Positive Reviews more than Negative Reviews")


das %>% ggplot(aes(y = (Po_S-Ne_S), x = reorder(Category, ((Po_S-Ne_S))), fill = Category))+geom_bar(stat = "identity")+theme(legend.position = "none")+coord_flip()+
  labs(y = "Relative polarity of Positive Reviews wrt Negative Reviews", x = "Category", title = "Relative polarity Positive Reviews wrt Negative Reviews")

rm(das)

Even though beauty has high negative reviews it has the highest polarity difference between positive and negative among all the categories. This category can be ignored due to low number of reviews.
Comics has the highest relative number of positive reviews compared to that of negative.

Almost all the categories have similar intensities when it comes to positive or negative reactions. Health and fitness received the most positive reactions compared to that of the negative ones.

Content Vs Reviews

das <- data %>% select(-c("Translated_Review", "App"))
das %>% ggplot(aes(x = as.factor(Content.Rating), fill = Content.Rating))+geom_histogram(stat = "count")+labs(y = "Number of Reviews", x = "Content Type", fill = "Content Type", title = "Number of Reviews per content")+scale_fill_viridis(discrete = TRUE)+facet_wrap(~Sentiment)+coord_flip()


das %>% ggplot(aes(x = as.factor(Content.Rating), y = Sentiment_Polarity, color = Content.Rating))+geom_boxplot()+labs(y = "Polarity", x = "Content Type", title = "Content Type Vs the intensity of reviews")


das %>% ggplot(aes(x = as.factor(Content.Rating), fill = Sentiment))+geom_bar(stat = "count", position = "fill")+labs(y = "Number of Reviews", x = "Content Type", fill = "Content Type", title = "Number of Reviews per content")+scale_fill_viridis(discrete = TRUE)

As expected there Everyone has the highest number of reviews in all categories. This could be because the it has the highest number of apps in the market.
Adult followed by Mature Apps have higher number of strong positive reviews while Teens have the higher number of strong negative reviews.

Machine Learning - Pre processing

Here apart from general preprocessing, I am defining the success to be installations equal to or greater than 500,000 i.e. the app must take 0.00034% of the total market downloads we classify result to be a “hit”, and else it’s “flop”.
The correlation among various variable is as follows:

rm(das)
rm(data)
data <- gps %>% select(c("Category", "Rating", "Reviews", "Size", "Type", "Content.Rating", "Current.Ver", "Installs"))
glimpse(data)
Observations: 8,892
Variables: 8
$ Category       <fct> ART_AND_DESIGN, ART_AND_DE...
$ Rating         <dbl> 4.1, 3.9, 4.7, 4.5, 4.3, 4...
$ Reviews        <fct> 159, 967, 87510, 215644, 9...
$ Size           <dbl> 19.0, 14.0, 8.7, 25.0, 2.8...
$ Type           <fct> Free, Free, Free, Free, Fr...
$ Content.Rating <fct> Everyone, Everyone, Everyo...
$ Current.Ver    <chr> "1", "2", "1", "Varies wit...
$ Installs       <fct> 10000, 500000, 5000000, 50...
data$Reviews <- as.integer(as.character(data$Reviews))
data$Current.Ver <- as.integer(as.character(data$Current.Ver))
data$Installs <- as.integer(as.character(data$Installs))
data <- data %>% na.omit()
data$Rating <- as.numeric(as.character(data$Rating))
glimpse(data)
Observations: 7,238
Variables: 8
$ Category       <fct> ART_AND_DESIGN, ART_AND_DE...
$ Rating         <dbl> 4.1, 3.9, 4.7, 4.3, 4.4, 3...
$ Reviews        <int> 159, 967, 87510, 967, 167,...
$ Size           <dbl> 19.0, 14.0, 8.7, 2.8, 5.6,...
$ Type           <fct> Free, Free, Free, Free, Fr...
$ Content.Rating <fct> Everyone, Everyone, Everyo...
$ Current.Ver    <int> 1, 2, 1, 1, 1, 1, 6, 2, 2,...
$ Installs       <int> 10000, 500000, 5000000, 10...
cor(data %>% select(c("Rating", "Reviews", "Size", "Current.Ver", "Installs")))
                  Rating      Reviews        Size
Rating       1.000000000  0.080283728 -0.02059633
Reviews      0.080283728  1.000000000  0.03373306
Size        -0.020596331  0.033733058  1.00000000
Current.Ver -0.002349692 -0.005979475  0.02677096
Installs     0.053719879  0.643536646  0.01538013
             Current.Ver     Installs
Rating      -0.002349692  0.053719879
Reviews     -0.005979475  0.643536646
Size         0.026770959  0.015380130
Current.Ver  1.000000000 -0.005627174
Installs    -0.005627174  1.000000000
corrplot(cor(data %>% select(c("Rating", "Reviews", "Size", "Current.Ver", "Installs"))), method = "color", title = "Correlation Plot", mar=c(0,0,1,0))


data <- data %>% mutate(Result = as.factor(ifelse(Installs >= 500000, "Hit", "Flop"))) %>% select(-Installs)

Installs has the highest correlation with reviews

Machine Learning - Data Partition

normalize <- function(x){
  return ((x-min(x))/(max(x)-min(x)))
}
data.ml<- cbind(as.data.frame(lapply(data[,c(2,3,4,7)],normalize)), data[,-c(2,3,4,7)])

set.seed(123)
trainindex <- createDataPartition(data.ml$Result, p=0.8, list= FALSE)
trainn <- data.ml[trainindex, ]
testn <- data.ml[-trainindex, ]

set.seed(123)
trainindex <- createDataPartition(data.ml$Result, p=0.8, list= FALSE)
training <- data.ml[trainindex, ]
testing <- data.ml[-trainindex, ]

I split the data into two parts training and testing using data partition at 80% split. I then normalized the continuous variables of the data for models that require normalized input and added the data that is not normalized to models which don’t require normalized inputs.

I have used the following models on the data to classify the records as hit or flop:

Logistic Regression
Decision Trees
Random Forest
Naïve Bayes Classifier
SVM with Default Cost parameter
Tuned SVM (Couldn’t add it as it takes more than 12 hours to run)

These models were run upon all the attributes with Result (“Hit” or “Flop”) being the target attribute.

The outputs of the models were as follows:

Machine Learning - Modelling - Logistic Regression

rm(trainindex)
rm(data.ml)
train <- trainn
test <- testn
logit.reg <- glm(Result ~.,data = train, family = "binomial")
summary(logit.reg)

Call:
glm(formula = Result ~ ., family = "binomial", data = train)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-5.1087  -0.3290  -0.0005   0.0000   2.7745  

Coefficients:
                               Estimate  Std. Error
(Intercept)                   -10.93293  1455.39801
Rating                         -1.88838     0.39186
Reviews                     25958.79073   991.38262
Size                            0.32449     0.63443
Current.Ver                     1.08064     1.52771
CategoryAUTO_AND_VEHICLES       0.03749     0.72313
CategoryBEAUTY                  0.50437     0.81195
CategoryBOOKS_AND_REFERENCE    -0.34906     0.71308
CategoryBUSINESS               -0.58562     0.67192
CategoryCOMICS                 -1.56059     1.05006
CategoryCOMMUNICATION          -0.98232     0.79991
CategoryDATING                 -0.70588     0.76343
CategoryEDUCATION               0.76883     0.73797
CategoryENTERTAINMENT           0.17953     0.97639
CategoryEVENTS                 -1.53144     1.26783
CategoryFAMILY                 -0.35114     0.60144
CategoryFINANCE                -1.41404     0.68849
CategoryFOOD_AND_DRINK         -0.25462     0.77866
CategoryGAME                   -0.47244     0.63475
CategoryHEALTH_AND_FITNESS     -0.23195     0.67832
CategoryHOUSE_AND_HOME          0.77182     0.74567
CategoryLIBRARIES_AND_DEMO      0.05045     0.78375
CategoryLIFESTYLE              -0.40941     0.65481
CategoryMAPS_AND_NAVIGATION    -0.69873     0.84434
CategoryMEDICAL                -1.35866     0.70991
CategoryNEWS_AND_MAGAZINES     -0.71658     0.74933
CategoryPARENTING               0.54597     0.82074
CategoryPERSONALIZATION         0.19263     0.67100
CategoryPHOTOGRAPHY             0.44799     0.67702
CategoryPRODUCTIVITY           -0.39227     0.67103
CategorySHOPPING                0.15645     0.70957
CategorySOCIAL                 -0.74732     0.78432
CategorySPORTS                 -0.78227     0.68603
CategoryTOOLS                  -0.41091     0.62426
CategoryTRAVEL_AND_LOCAL        0.01793     0.69436
CategoryVIDEO_PLAYERS           0.00502     0.72942
CategoryWEATHER                 0.52329     0.82155
TypePaid                      -13.67281     0.94806
Content.RatingEveryone          9.74847  1455.39787
Content.RatingMature           10.02771  1455.39790
Content.RatingTeen              8.89360  1455.39787
Content.RatingUnrated          -3.44570  2058.24319
                            z value
(Intercept)                  -0.008
Rating                       -4.819
Reviews                      26.184
Size                          0.511
Current.Ver                   0.707
CategoryAUTO_AND_VEHICLES     0.052
CategoryBEAUTY                0.621
CategoryBOOKS_AND_REFERENCE  -0.490
CategoryBUSINESS             -0.872
CategoryCOMICS               -1.486
CategoryCOMMUNICATION        -1.228
CategoryDATING               -0.925
CategoryEDUCATION             1.042
CategoryENTERTAINMENT         0.184
CategoryEVENTS               -1.208
CategoryFAMILY               -0.584
CategoryFINANCE              -2.054
CategoryFOOD_AND_DRINK       -0.327
CategoryGAME                 -0.744
CategoryHEALTH_AND_FITNESS   -0.342
CategoryHOUSE_AND_HOME        1.035
CategoryLIBRARIES_AND_DEMO    0.064
CategoryLIFESTYLE            -0.625
CategoryMAPS_AND_NAVIGATION  -0.828
CategoryMEDICAL              -1.914
CategoryNEWS_AND_MAGAZINES   -0.956
CategoryPARENTING             0.665
CategoryPERSONALIZATION       0.287
CategoryPHOTOGRAPHY           0.662
CategoryPRODUCTIVITY         -0.585
CategorySHOPPING              0.220
CategorySOCIAL               -0.953
CategorySPORTS               -1.140
CategoryTOOLS                -0.658
CategoryTRAVEL_AND_LOCAL      0.026
CategoryVIDEO_PLAYERS         0.007
CategoryWEATHER               0.637
TypePaid                    -14.422
Content.RatingEveryone        0.007
Content.RatingMature          0.007
Content.RatingTeen            0.006
Content.RatingUnrated        -0.002
                                        Pr(>|z|)    
(Intercept)                               0.9940    
Rating                                0.00000144 ***
Reviews                     < 0.0000000000000002 ***
Size                                      0.6090    
Current.Ver                               0.4793    
CategoryAUTO_AND_VEHICLES                 0.9587    
CategoryBEAUTY                            0.5345    
CategoryBOOKS_AND_REFERENCE               0.6245    
CategoryBUSINESS                          0.3835    
CategoryCOMICS                            0.1372    
CategoryCOMMUNICATION                     0.2194    
CategoryDATING                            0.3552    
CategoryEDUCATION                         0.2975    
CategoryENTERTAINMENT                     0.8541    
CategoryEVENTS                            0.2271    
CategoryFAMILY                            0.5593    
CategoryFINANCE                           0.0400 *  
CategoryFOOD_AND_DRINK                    0.7437    
CategoryGAME                              0.4567    
CategoryHEALTH_AND_FITNESS                0.7324    
CategoryHOUSE_AND_HOME                    0.3006    
CategoryLIBRARIES_AND_DEMO                0.9487    
CategoryLIFESTYLE                         0.5318    
CategoryMAPS_AND_NAVIGATION               0.4079    
CategoryMEDICAL                           0.0556 .  
CategoryNEWS_AND_MAGAZINES                0.3389    
CategoryPARENTING                         0.5059    
CategoryPERSONALIZATION                   0.7741    
CategoryPHOTOGRAPHY                       0.5082    
CategoryPRODUCTIVITY                      0.5588    
CategorySHOPPING                          0.8255    
CategorySOCIAL                            0.3407    
CategorySPORTS                            0.2542    
CategoryTOOLS                             0.5104    
CategoryTRAVEL_AND_LOCAL                  0.9794    
CategoryVIDEO_PLAYERS                     0.9945    
CategoryWEATHER                           0.5242    
TypePaid                    < 0.0000000000000002 ***
Content.RatingEveryone                    0.9947    
Content.RatingMature                      0.9945    
Content.RatingTeen                        0.9951    
Content.RatingUnrated                     0.9987    
---
Signif. codes:  
0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 7960.4  on 5790  degrees of freedom
Residual deviance: 1925.0  on 5749  degrees of freedom
AIC: 2009

Number of Fisher Scoring iterations: 14
logit.reg <- glm(Result ~.,data = train %>% select(-"Category"), family = "binomial")
summary(logit.reg)

Call:
glm(formula = Result ~ ., family = "binomial", data = train %>% 
    select(-"Category"))

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-5.0934  -0.3408  -0.0006   0.0000   2.6391  

Coefficients:
                         Estimate Std. Error z value
(Intercept)              -11.6063  1455.3979  -0.008
Rating                    -1.8041     0.3704  -4.870
Reviews                25796.9828   957.4169  26.944
Size                       0.2645     0.6097   0.434
Current.Ver                0.9836     1.4986   0.656
TypePaid                 -13.3421     0.9482 -14.071
Content.RatingEveryone    10.0192  1455.3979   0.007
Content.RatingMature      10.1071  1455.3979   0.007
Content.RatingTeen         9.1242  1455.3979   0.006
Content.RatingUnrated     -3.2440  2058.2432  -0.002
                                   Pr(>|z|)    
(Intercept)                           0.994    
Rating                           0.00000112 ***
Reviews                < 0.0000000000000002 ***
Size                                  0.664    
Current.Ver                           0.512    
TypePaid               < 0.0000000000000002 ***
Content.RatingEveryone                0.995    
Content.RatingMature                  0.994    
Content.RatingTeen                    0.995    
Content.RatingUnrated                 0.999    
---
Signif. codes:  
0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 7960.4  on 5790  degrees of freedom
Residual deviance: 1984.3  on 5781  degrees of freedom
AIC: 2004.3

Number of Fisher Scoring iterations: 14
#confint(logit.reg) to check for confidence intervals

test$predraw <- predict(logit.reg, newdata = test %>% select(-c("Category", "Result")), type = "response")

par <- data.frame("Cut off" = 1, "Accuracy" = 1, "Sensitivity" = 1)
i = 0
j = 1
for(i in seq(0,1,0.001)){
  test <- test %>% mutate(pred = factor(as.character(ifelse(predraw < i, "Flop", "Hit")), levels = c("Hit", "Flop")))
 par[j,2] <- 1-mean(test$Result != test$pred)
 par[j,1] <- i
 test1 <- test %>% filter(Result == "Hit")
  par[j,3] <- mean(test1$Result == test1$pred)
j <- j+1
i <- i+1
rm(test1)
}
rm(i)
rm(j)

kable(par %>% filter(Accuracy == max(Accuracy)) %>% head(5), caption = "The cut off for the best accuracy")

Cut.off Accuracy Sensitivity
0.370 0.9502419 0.9162791
0.371 0.9502419 0.9162791
0.372 0.9502419 0.9147287
0.373 0.9502419 0.9147287
0.374 0.9502419 0.9147287


par %>%ggplot(aes(x = Cut.off, y = par$Accuracy))+geom_point(size = 0.05, color = "gray")+geom_line()+geom_hline(yintercept = max(par$Accuracy), color = "red")+geom_vline(xintercept = 0.375, color = "red")+
  labs(x = "Cut Off value", y = "Accuracy of prediction", title = "Cut off Vs Accuracy")

rm(par)  

test <- test %>% mutate(pred = as.factor(ifelse(predraw <= 0.35, "Flop", "Hit")))
test$pred <- factor(test$pred, levels = c("Hit", "Flop"))
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))
cm <- confusionMatrix(test$pred, test$Result)
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  591   22
      Flop  54  780
                                               
               Accuracy : 0.9475               
                 95% CI : (0.9347, 0.9584)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : < 0.00000000000000022
                                               
                  Kappa : 0.8932               
                                               
 Mcnemar's Test P-Value : 0.0003766            
                                               
            Sensitivity : 0.9163               
            Specificity : 0.9726               
         Pos Pred Value : 0.9641               
         Neg Pred Value : 0.9353               
             Prevalence : 0.4457               
         Detection Rate : 0.4084               
   Detection Prevalence : 0.4236               
      Balanced Accuracy : 0.9444               
                                               
       'Positive' Class : Hit                  
                                               
logit <- data.frame("Model" = "Logistic Regression", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])

Logistic regression has performed pretty well in all the aspects. Note: It must remembered that our class of interest is Hit.

Machine Learning - Modelling - Decision Trees

rm(test)
rm(train)
rm(logit.reg)
train <- training
test <- testing

set.seed(123)
dt <- rpart(Result ~., data = train, method = "class", cp = 0.00001, minsplit = 1, xval = 10)
dt.pruned <- prune(dt, cp = 0.004)

test$pred <- predict(dt.pruned, newdata = test[,-8], type = "class")

cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  593   28
      Flop  52  774
                                              
               Accuracy : 0.9447              
                 95% CI : (0.9317, 0.9559)    
    No Information Rate : 0.5543              
    P-Value [Acc > NIR] : < 0.0000000000000002
                                              
                  Kappa : 0.8877              
                                              
 Mcnemar's Test P-Value : 0.01013             
                                              
            Sensitivity : 0.9194              
            Specificity : 0.9651              
         Pos Pred Value : 0.9549              
         Neg Pred Value : 0.9370              
             Prevalence : 0.4457              
         Detection Rate : 0.4098              
   Detection Prevalence : 0.4292              
      Balanced Accuracy : 0.9422              
                                              
       'Positive' Class : Hit                 
                                              
det <- data.frame("Model" = "Decision Trees", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])
rm(cm)
rpart.plot(dt.pruned, box.palette="RdBu", shadow.col="gray", nn=TRUE)


rpart.rules(dt.pruned, style = "tallw")
Result is 0.00 when
               Reviews is 0.00009 to 0.00019
               Type is Paid

Result is 0.00 when
               Reviews is 0.00019 to 0.00053
               Type is Paid

Result is 0.07 when
               Reviews < 0.00009

Result is 0.68 when
               Reviews is 0.00009 to 0.00019
               Type is Free

Result is 0.80 when
               Reviews >= 0.00053
               Type is Paid

Result is 0.98 when
               Reviews >= 0.00019
               Type is Free

Machine Learning - Modelling - Random Forest

rm(train)
rm(test)
rm(dt)
rm(dt.pruned)
train <- training
test <- testing
rf <- randomForest(Result ~., data = train)
plot(rf, main = "Error rate of random forest")

varImpPlot(rf, main = "Gini Plot - Importance of Variables")

test$pred = predict(rf, newdata = test[-8])
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  613   44
      Flop  32  758
                                             
               Accuracy : 0.9475             
                 95% CI : (0.9347, 0.9584)   
    No Information Rate : 0.5543             
    P-Value [Acc > NIR] : <0.0000000000000002
                                             
                  Kappa : 0.8939             
                                             
 Mcnemar's Test P-Value : 0.207              
                                             
            Sensitivity : 0.9504             
            Specificity : 0.9451             
         Pos Pred Value : 0.9330             
         Neg Pred Value : 0.9595             
             Prevalence : 0.4457             
         Detection Rate : 0.4236             
   Detection Prevalence : 0.4540             
      Balanced Accuracy : 0.9478             
                                             
       'Positive' Class : Hit                
                                             
raf <- data.frame("Model" = "Random Forest", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])
rm(cm)

Machine Learning - Modelling - Naive Bayes Classifier

rm(test)
rm(train)
rm(rf)
train <- training
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

delays.nb <- naiveBayes(Result~., data = train)
delays.nb

Naive Bayes Classifier for Discrete Predictors

Call:
naiveBayes.default(x = X, y = Y, laplace = laplace)

A-priori probabilities:
Y
     Hit     Flop 
0.446037 0.553963 

Conditional probabilities:
      Rating
Y           [,1]       [,2]
  Hit  0.8122145 0.08782847
  Flop 0.7745324 0.16554476

      Reviews
Y               [,1]          [,2]
  Hit  0.01358894493 0.05288638212
  Flop 0.00002235907 0.00006197387

      Size
Y            [,1]       [,2]
  Hit  0.03556935 0.06165665
  Flop 0.03563030 0.10756489

      Current.Ver
Y              [,1]       [,2]
  Hit  0.0009565182 0.02834286
  Flop 0.0017979409 0.03567101

      Category
Y      ART_AND_DESIGN AUTO_AND_VEHICLES       BEAUTY
  Hit    0.0050329075      0.0061943477 0.0034843206
  Flop   0.0093516209      0.0127805486 0.0065461347
      Category
Y      BOOKS_AND_REFERENCE     BUSINESS       COMICS
  Hit         0.0154858691 0.0158730159 0.0034843206
  Flop        0.0233790524 0.0417705736 0.0084164589
      Category
Y      COMMUNICATION       DATING    EDUCATION
  Hit   0.0313588850 0.0174216028 0.0174216028
  Flop  0.0249376559 0.0224438903 0.0068578554
      Category
Y      ENTERTAINMENT       EVENTS       FAMILY
  Hit   0.0178087495 0.0007742935 0.2040263260
  Flop  0.0028054863 0.0068578554 0.2319201995
      Category
Y           FINANCE FOOD_AND_DRINK         GAME
  Hit  0.0240030972   0.0147115757 0.1966705381
  Flop 0.0464463840   0.0102867830 0.0692019950
      Category
Y      HEALTH_AND_FITNESS HOUSE_AND_HOME
  Hit        0.0282617112   0.0096786682
  Flop       0.0246259352   0.0046758105
      Category
Y      LIBRARIES_AND_DEMO    LIFESTYLE
  Hit        0.0054200542 0.0247773906
  Flop       0.0112219451 0.0458229426
      Category
Y      MAPS_AND_NAVIGATION      MEDICAL
  Hit         0.0116144019 0.0081300813
  Flop        0.0130922693 0.0620324190
      Category
Y      NEWS_AND_MAGAZINES    PARENTING
  Hit        0.0197444832 0.0038714673
  Flop       0.0240024938 0.0059226933
      Category
Y      PERSONALIZATION  PHOTOGRAPHY PRODUCTIVITY
  Hit     0.0356174990 0.0460704607 0.0321331785
  Flop    0.0386533666 0.0174563591 0.0308603491
      Category
Y          SHOPPING       SOCIAL       SPORTS
  Hit  0.0329074719 0.0228416570 0.0301974448
  Flop 0.0124688279 0.0208852868 0.0317955112
      Category
Y             TOOLS TRAVEL_AND_LOCAL VIDEO_PLAYERS
  Hit  0.0662020906     0.0205187766  0.0185830430
  Flop 0.0950748130     0.0202618454  0.0124688279
      Category
Y           WEATHER
  Hit  0.0096786682
  Flop 0.0046758105

      Type
Y                0        Free         NaN
  Hit  0.000000000 0.992257065 0.000000000
  Flop 0.000000000 0.875311721 0.000000000
      Type
Y             Paid
  Hit  0.007742935
  Flop 0.124688279

      Content.Rating
Y            Adults     Everyone       Mature
  Hit  0.0003871467 0.8033294619 0.0514905149
  Flop 0.0000000000 0.8753117207 0.0405236908
      Content.Rating
Y              Teen      Unrated
  Hit  0.1447928765 0.0000000000
  Flop 0.0838528678 0.0003117207
pred.prob <- as.data.frame(predict(delays.nb, newdata = test, type = "raw"))
pred.prob <-  round(pred.prob, 4)
pred.prob <- pred.prob %>% select("Hit")
names(pred.prob) <-  "predraw"
test <- cbind(test, pred.prob)
rm(pred.prob)

par <- data.frame("Cut off" = 1, "Accuracy" = 1, "Sensitivity" = 1)
i = 0
j = 1
for(i in seq(0,1,0.001)){
  test <- test %>% mutate(pred = factor(as.character(ifelse(predraw < i, "Flop", "Hit")), levels = c("Hit", "Flop")))
 par[j,2] <- 1-mean(test$Result != test$pred)
 test1 <- test %>% filter(Result == "Hit")
 par[j,3] <- mean(test1$Result == test1$pred)
 par[j,1] <- i
j <- j+1
i <- i+1
rm(test1)
}
rm(i)
rm(j)

kable(par %>% filter(Accuracy == max(Accuracy)) %>% head(5), caption = "The (Hit) probability cut off for the best accuracy")

Cut.off Accuracy Sensitivity
0.022 0.9246717 0.8573643
0.023 0.9246717 0.8573643
0.024 0.9246717 0.8573643
0.028 0.9246717 0.8542636


par %>% ggplot(aes(x = `Cut.off`, y = par$Accuracy))+geom_point(size = 0.05, color = "gray")+geom_line()+geom_hline(yintercept = max(par$Accuracy), color = "red")+geom_vline(xintercept = 0.024, color = "red")+
  labs(x = "Cut Off value", y = "Accuracy of prediction", title = "Cut off Vs Accuracy")


rm(par)

test <- test %>% mutate(pred =  factor(as.character(ifelse(predraw < 0.024, "Flop", "Hit")), levels = c("Hit", "Flop")))

cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  553   17
      Flop  92  785
                                               
               Accuracy : 0.9247               
                 95% CI : (0.9098, 0.9377)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : < 0.00000000000000022
                                               
                  Kappa : 0.8458               
                                               
 Mcnemar's Test P-Value : 0.000000000001361    
                                               
            Sensitivity : 0.8574               
            Specificity : 0.9788               
         Pos Pred Value : 0.9702               
         Neg Pred Value : 0.8951               
             Prevalence : 0.4457               
         Detection Rate : 0.3822               
   Detection Prevalence : 0.3939               
      Balanced Accuracy : 0.9181               
                                               
       'Positive' Class : Hit                  
                                               
nab <- data.frame("Model" = "Naive Bayes", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])
rm(cm)

Machine Learning - Modelling - SVM

rm(test)
rm(train)
rm(delays.nb)

train <- training
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

set.seed(123)
svm.fit <- svm(Result~., data=train, kernel = "linear")

test$pred <- predict(svm.fit, newdata = test[,-8], type = "class")
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  376   40
      Flop 269  762
                                               
               Accuracy : 0.7865               
                 95% CI : (0.7644, 0.8073)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : < 0.00000000000000022
                                               
                  Kappa : 0.5523               
                                               
 Mcnemar's Test P-Value : < 0.00000000000000022
                                               
            Sensitivity : 0.5829               
            Specificity : 0.9501               
         Pos Pred Value : 0.9038               
         Neg Pred Value : 0.7391               
             Prevalence : 0.4457               
         Detection Rate : 0.2598               
   Detection Prevalence : 0.2875               
      Balanced Accuracy : 0.7665               
                                               
       'Positive' Class : Hit                  
                                               
linear <- data.frame("MOdel" = "SVM - Linear", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])

rm(test)
rm(train)
rm(svm.fit)
rm(cm)

train <- training
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

set.seed(123)
svm.fit <- svm(Result~., data=train, kernel = "radial")

test$pred <- predict(svm.fit, newdata = test[,-8], type = "class")
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  343   93
      Flop 302  709
                                               
               Accuracy : 0.727                
                 95% CI : (0.7033, 0.7498)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : < 0.00000000000000022
                                               
                  Kappa : 0.4294               
                                               
 Mcnemar's Test P-Value : < 0.00000000000000022
                                               
            Sensitivity : 0.5318               
            Specificity : 0.8840               
         Pos Pred Value : 0.7867               
         Neg Pred Value : 0.7013               
             Prevalence : 0.4457               
         Detection Rate : 0.2370               
   Detection Prevalence : 0.3013               
      Balanced Accuracy : 0.7079               
                                               
       'Positive' Class : Hit                  
                                               
radial <- data.frame("MOdel" = "SVM - Radial", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])

rm(test)
rm(train)
rm(svm.fit)
rm(cm)

train <- training
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

set.seed(123)
svm.fit <- svm(Result~., data=train, kernel = "polynomial")

test$pred <- predict(svm.fit, newdata = test[,-8], type = "class")
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit   22    0
      Flop 623  802
                                             
               Accuracy : 0.5695             
                 95% CI : (0.5435, 0.5951)   
    No Information Rate : 0.5543             
    P-Value [Acc > NIR] : 0.1277             
                                             
                  Kappa : 0.0377             
                                             
 Mcnemar's Test P-Value : <0.0000000000000002
                                             
            Sensitivity : 0.03411            
            Specificity : 1.00000            
         Pos Pred Value : 1.00000            
         Neg Pred Value : 0.56281            
             Prevalence : 0.44575            
         Detection Rate : 0.01520            
   Detection Prevalence : 0.01520            
      Balanced Accuracy : 0.51705            
                                             
       'Positive' Class : Hit                
                                             
polynomial <- data.frame("MOdel" = "SVM - Polynomial", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])

rm(cm)
rm(test)
rm(train)
rm(svm.fit)

svm <- rbind(linear, radial, polynomial)
rm(linear)
rm(radial)
rm(polynomial)
names(svm) <- c("Model", "Accuracy", "Sensitivity", "Specificity")

Machine Learning - Modelling - SVM (Tuning)

set.seed(123)
tunesvm <- tune(svm, Result~., data = train, kernal = "linear", ranges = list(cost = c(seq(0.01,0.1,by = 0.01), seq(0.1,1,by = 0.1), seq(1,10,by = 1))))
dynamic.cost <- tunesvm$best.model$cost
summary(tunesvm)
data.par <- tunesvm$performances
data.par[data.par$error == min(data.par$error),]

set.seed(123)
svm_cost <- svm(Result~., data=train,kernel = "linear", cost = dynamic.cost)
pred_train <- predict(svm_cost, train)
train.error <- mean(pred_train != train$Result)
pred_test <- predict(svm_cost, test)
test.error <- mean(pred_test != test$Result)
linear <- data.frame("Kernal" = "Linear", "Cost" = dynamic.cost,
                     "train Error" = train.error, 
                     "test Error" = test.error)

set.seed(123)
tunesvm <- tune(svm, Result~., data = train, kernal = "radial", ranges = list(cost = c(seq(0.01,0.1,by = 0.01), seq(0.1,1,by = 0.1), seq(1,10,by = 1))))
dynamic.cost <- tunesvm$best.model$cost
summary(tunesvm)
data.par <- tunesvm$performances
data.par[data.par$error == min(data.par$error),] 


svm_cost <- svm(Result~., data=train, kernel = "radial", cost = dynamic.cost)
pred_train <- predict(svm_cost, train)
train.error <- mean(pred_train != train$Result)
pred_test <- predict(svm_cost, test)
test.error <- mean(pred_test != test$Result)
radial <- data.frame("Kernal" = "Radial", "Cost" = dynamic.cost,
                     "train Error" =  train.error, 
                     "test Error" = test.error)

set.seed(123)
tunesvm <- tune(svm, Result~., data = train, kernal = "polynomial", degree = 2, ranges = list(cost = c(seq(0.01,0.1, by = 0.01), seq(0.1,1,by = 0.1), seq(1,10,by = 1))))
dynamic.cost <- tunesvm$best.model$cost
summary(tunesvm)
data.par <- tunesvm$performances
data.par[data.par$error == min(data.par$error),]
svm_cost <- svm(Result~., data=train, kernel = "polynomial", degree = 2, cost = dynamic.cost)
pred_train <- predict(svm_cost, train)
train.error <- mean(pred_train != train$Result)
pred_test <- predict(svm_cost, test)
test.error <- mean(pred_test != test$Result)
polynomial <- data.frame("Kernal" = "Polynomial", "Cost" = dynamic.cost,
                         "train Error" =  train.error, 
                         "test Error" = test.error)


kable(final <- rbind(linear, radial, polynomial), caption = "SVM Performance with different kernals")

In this analysis I used Rating and Reviews which are known only after an App is launched. So the analysis I did so far is purely hypothetical. For this reason I am repeating the entire analysis dropping Rating and Reviews Attributes.

Machine Learning - Modelling - Logistic Regression

rm(trainindex)
rm(data.ml)
train <- trainn %>% select(-c("Rating", "Reviews"))
test <- testn %>% select(-c("Rating", "Reviews"))

logit.reg <- glm(Result ~.,data = train, family = "binomial")
summary(logit.reg)

Call:
glm(formula = Result ~ ., family = "binomial", data = train)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-1.9539  -1.0772  -0.3188   1.1647   2.9656  

Coefficients:
                              Estimate Std. Error
(Intercept)                  10.932973 196.968026
Size                          0.290329   0.343408
Current.Ver                  -1.065856   0.955384
CategoryAUTO_AND_VEHICLES    -0.166999   0.446399
CategoryBEAUTY               -0.078609   0.520725
CategoryBOOKS_AND_REFERENCE   0.198912   0.389295
CategoryBUSINESS             -0.387421   0.380052
CategoryCOMICS               -0.481986   0.513833
CategoryCOMMUNICATION         0.884703   0.373083
CategoryDATING                0.028899   0.420699
CategoryEDUCATION             1.603696   0.430444
CategoryENTERTAINMENT         2.278416   0.497425
CategoryEVENTS               -1.646004   0.811304
CategoryFAMILY                0.540242   0.340662
CategoryFINANCE              -0.034169   0.368334
CategoryFOOD_AND_DRINK        0.929963   0.412082
CategoryGAME                  1.751529   0.347747
CategoryHEALTH_AND_FITNESS    0.755244   0.373784
CategoryHOUSE_AND_HOME        1.275349   0.468070
CategoryLIBRARIES_AND_DEMO   -0.227878   0.466059
CategoryLIFESTYLE             0.003346   0.367949
CategoryMAPS_AND_NAVIGATION   0.499621   0.413614
CategoryMEDICAL              -1.259244   0.407924
CategoryNEWS_AND_MAGAZINES    0.351646   0.381546
CategoryPARENTING             0.162152   0.517472
CategoryPERSONALIZATION       0.894042   0.368283
CategoryPHOTOGRAPHY           1.694726   0.376429
CategoryPRODUCTIVITY          0.701663   0.368215
CategorySHOPPING              1.533020   0.387390
CategorySOCIAL                0.502244   0.384332
CategorySPORTS                0.621386   0.369610
CategoryTOOLS                 0.328361   0.349381
CategoryTRAVEL_AND_LOCAL      0.653622   0.384622
CategoryVIDEO_PLAYERS         0.925326   0.397957
CategoryWEATHER               1.610839   0.491177
TypePaid                     -3.079504   0.235667
Content.RatingEveryone      -11.714243 196.967741
Content.RatingMature        -11.409212 196.967821
Content.RatingTeen          -11.479430 196.967763
Content.RatingUnrated       -22.828966 278.554424
                            z value
(Intercept)                   0.056
Size                          0.845
Current.Ver                  -1.116
CategoryAUTO_AND_VEHICLES    -0.374
CategoryBEAUTY               -0.151
CategoryBOOKS_AND_REFERENCE   0.511
CategoryBUSINESS             -1.019
CategoryCOMICS               -0.938
CategoryCOMMUNICATION         2.371
CategoryDATING                0.069
CategoryEDUCATION             3.726
CategoryENTERTAINMENT         4.580
CategoryEVENTS               -2.029
CategoryFAMILY                1.586
CategoryFINANCE              -0.093
CategoryFOOD_AND_DRINK        2.257
CategoryGAME                  5.037
CategoryHEALTH_AND_FITNESS    2.021
CategoryHOUSE_AND_HOME        2.725
CategoryLIBRARIES_AND_DEMO   -0.489
CategoryLIFESTYLE             0.009
CategoryMAPS_AND_NAVIGATION   1.208
CategoryMEDICAL              -3.087
CategoryNEWS_AND_MAGAZINES    0.922
CategoryPARENTING             0.313
CategoryPERSONALIZATION       2.428
CategoryPHOTOGRAPHY           4.502
CategoryPRODUCTIVITY          1.906
CategorySHOPPING              3.957
CategorySOCIAL                1.307
CategorySPORTS                1.681
CategoryTOOLS                 0.940
CategoryTRAVEL_AND_LOCAL      1.699
CategoryVIDEO_PLAYERS         2.325
CategoryWEATHER               3.280
TypePaid                    -13.067
Content.RatingEveryone       -0.059
Content.RatingMature         -0.058
Content.RatingTeen           -0.058
Content.RatingUnrated        -0.082
                                        Pr(>|z|)    
(Intercept)                             0.955735    
Size                                    0.397869    
Current.Ver                             0.264580    
CategoryAUTO_AND_VEHICLES               0.708328    
CategoryBEAUTY                          0.880007    
CategoryBOOKS_AND_REFERENCE             0.609383    
CategoryBUSINESS                        0.308018    
CategoryCOMICS                          0.348234    
CategoryCOMMUNICATION                   0.017724 *  
CategoryDATING                          0.945234    
CategoryEDUCATION                       0.000195 ***
CategoryENTERTAINMENT                0.000004640 ***
CategoryEVENTS                          0.042475 *  
CategoryFAMILY                          0.112771    
CategoryFINANCE                         0.926090    
CategoryFOOD_AND_DRINK                  0.024024 *  
CategoryGAME                         0.000000473 ***
CategoryHEALTH_AND_FITNESS              0.043328 *  
CategoryHOUSE_AND_HOME                  0.006436 ** 
CategoryLIBRARIES_AND_DEMO              0.624880    
CategoryLIFESTYLE                       0.992744    
CategoryMAPS_AND_NAVIGATION             0.227070    
CategoryMEDICAL                         0.002022 ** 
CategoryNEWS_AND_MAGAZINES              0.356719    
CategoryPARENTING                       0.754012    
CategoryPERSONALIZATION                 0.015199 *  
CategoryPHOTOGRAPHY                  0.000006728 ***
CategoryPRODUCTIVITY                    0.056704 .  
CategorySHOPPING                     0.000075801 ***
CategorySOCIAL                          0.191282    
CategorySPORTS                          0.092725 .  
CategoryTOOLS                           0.347302    
CategoryTRAVEL_AND_LOCAL                0.089246 .  
CategoryVIDEO_PLAYERS                   0.020062 *  
CategoryWEATHER                         0.001040 ** 
TypePaid                    < 0.0000000000000002 ***
Content.RatingEveryone                  0.952575    
Content.RatingMature                    0.953809    
Content.RatingTeen                      0.953525    
Content.RatingUnrated                   0.934682    
---
Signif. codes:  
0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 7960.4  on 5790  degrees of freedom
Residual deviance: 6978.9  on 5751  degrees of freedom
AIC: 7058.9

Number of Fisher Scoring iterations: 10
#confint(logit.reg) to check for confidence intervals

test$predraw <- predict(logit.reg, newdata = test %>% select(-"Result"), type = "response")

par <- data.frame("Cut off" = 1, "Accuracy" = 1, "Sensitivity" = 1)
i = 0
j = 1
for(i in seq(0,1,0.001)){
  test <- test %>% mutate(pred = factor(as.character(ifelse(predraw < i, "Flop", "Hit")), levels = c("Hit", "Flop")))
 par[j,2] <- 1-mean(test$Result != test$pred)
 par[j,1] <- i
 test1 <- test %>% filter(Result == "Hit")
  par[j,3] <- mean(test1$Result == test1$pred)
j <- j+1
i <- i+1
rm(test1)
}
rm(i)
rm(j)

kable(par %>% filter(Accuracy == max(Accuracy)) %>% head(5), caption = "The cut off for the best accuracy")

Cut.off Accuracy Sensitivity
0.443 0.6862474 0.696124


par %>%ggplot(aes(x = Cut.off, y = par$Accuracy))+geom_point(size = 0.05, color = "gray")+geom_line()+geom_hline(yintercept = max(par$Accuracy), color = "red")+geom_vline(xintercept = 0.443, color = "red")+
  labs(x = "Cut Off value", y = "Accuracy of prediction", title = "Cut off Vs Accuracy")

rm(par)  

test <- test %>% mutate(pred = as.factor(ifelse(predraw <= 0.443, "Flop", "Hit")))
test$pred <- factor(test$pred, levels = c("Hit", "Flop"))
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))
cm <- confusionMatrix(test$pred, test$Result)
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  449  258
      Flop 196  544
                                               
               Accuracy : 0.6862               
                 95% CI : (0.6616, 0.7101)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : < 0.00000000000000022
                                               
                  Kappa : 0.3709               
                                               
 Mcnemar's Test P-Value : 0.004198             
                                               
            Sensitivity : 0.6961               
            Specificity : 0.6783               
         Pos Pred Value : 0.6351               
         Neg Pred Value : 0.7351               
             Prevalence : 0.4457               
         Detection Rate : 0.3103               
   Detection Prevalence : 0.4886               
      Balanced Accuracy : 0.6872               
                                               
       'Positive' Class : Hit                  
                                               
logit.u <- data.frame("Model" = "Logistic Regression (U)", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])
rm(cm)

Machine Learning - Modelling - Decision Trees

rm(test)
rm(train)
rm(logit.reg)
train <- training %>% select(-c("Rating", "Reviews"))
test <- testing %>% select(-c("Rating", "Reviews"))

set.seed(123)
dt <- rpart(Result ~., data = train, method = "class", cp = 0.00001, minsplit = 1, xval = 10)
dt.pruned <- prune(dt, cp = 0.004)

test$pred <- predict(dt.pruned, newdata = test[,-6], type = "class")

cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  468  261
      Flop 177  541
                                               
               Accuracy : 0.6973               
                 95% CI : (0.6729, 0.7209)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : < 0.00000000000000022
                                               
                  Kappa : 0.3951               
                                               
 Mcnemar's Test P-Value : 0.00007312           
                                               
            Sensitivity : 0.7256               
            Specificity : 0.6746               
         Pos Pred Value : 0.6420               
         Neg Pred Value : 0.7535               
             Prevalence : 0.4457               
         Detection Rate : 0.3234               
   Detection Prevalence : 0.5038               
      Balanced Accuracy : 0.7001               
                                               
       'Positive' Class : Hit                  
                                               
det.u <- data.frame("Model" = "Decision Tress (U)", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])
rm(cm)
rpart.plot(dt.pruned, box.palette="RdBu", shadow.col="gray", nn=TRUE)


rpart.rules(dt.pruned, style = "tallw")
Result is 0.00 when
               Category is LIBRARIES_AND_DEMO or MEDICAL
               Size >= 0.0307
               Current.Ver >= 0.000000127

Result is 0.03 when
               Category is ART_AND_DESIGN or AUTO_AND_VEHICLES or BEAUTY or BOOKS_AND_REFERENCE or BUSINESS or COMICS or COMMUNICATION or DATING or EVENTS or FAMILY or FINANCE or HEALTH_AND_FITNESS or LIBRARIES_AND_DEMO or LIFESTYLE or MAPS_AND_NAVIGATION or MEDICAL or NEWS_AND_MAGAZINES or PARENTING or PERSONALIZATION or PRODUCTIVITY or SOCIAL or SPORTS or TOOLS or TRAVEL_AND_LOCAL
               Current.Ver < 0.000000127
               Type is Paid

Result is 0.08 when
               Category is EDUCATION or ENTERTAINMENT or FOOD_AND_DRINK or GAME or HOUSE_AND_HOME or PHOTOGRAPHY or SHOPPING or VIDEO_PLAYERS or WEATHER
               Type is Paid

Result is 0.14 when
               Category is ART_AND_DESIGN or BEAUTY or BOOKS_AND_REFERENCE or COMMUNICATION or FAMILY or PERSONALIZATION or SPORTS
               Size >= 0.1269
               Current.Ver < 0.000000127
               Type is Free

Result is 0.16 when
               Category is AUTO_AND_VEHICLES or BUSINESS or DATING or FAMILY or LIFESTYLE or PARENTING or SPORTS
               Size < 0.0078
               Current.Ver >= 0.000000127

Result is 0.16 when
               Category is ART_AND_DESIGN or BEAUTY or EVENTS or FINANCE or LIBRARIES_AND_DEMO or MEDICAL
               Size < 0.0307
               Current.Ver >= 0.000000127

Result is 0.21 when
               Category is AUTO_AND_VEHICLES or BOOKS_AND_REFERENCE or BUSINESS or COMICS or COMMUNICATION or DATING or FAMILY or HEALTH_AND_FITNESS or LIFESTYLE or MAPS_AND_NAVIGATION or NEWS_AND_MAGAZINES or PARENTING or PERSONALIZATION or PRODUCTIVITY or SOCIAL or SPORTS or TOOLS or TRAVEL_AND_LOCAL
               Size >= 0.0078
               Current.Ver >= 0.000000127
               Type is Paid

Result is 0.24 when
               Category is AUTO_AND_VEHICLES or BUSINESS or COMICS or DATING or EVENTS or FINANCE or HEALTH_AND_FITNESS or LIBRARIES_AND_DEMO or LIFESTYLE or MAPS_AND_NAVIGATION or MEDICAL or NEWS_AND_MAGAZINES or PARENTING or PRODUCTIVITY or SOCIAL or TOOLS or TRAVEL_AND_LOCAL
               Size >= 0.0126
               Current.Ver < 0.000000091
               Type is Free

Result is 0.26 when
               Category is ART_AND_DESIGN or AUTO_AND_VEHICLES or BEAUTY or BOOKS_AND_REFERENCE or BUSINESS or COMICS or COMMUNICATION or DATING or EVENTS or FAMILY or FINANCE or HEALTH_AND_FITNESS or LIBRARIES_AND_DEMO or LIFESTYLE or MAPS_AND_NAVIGATION or MEDICAL or NEWS_AND_MAGAZINES or PARENTING or PERSONALIZATION or PRODUCTIVITY or SOCIAL or SPORTS or TOOLS or TRAVEL_AND_LOCAL
               Size < 0.0126
               Current.Ver < 0.000000127
               Type is Free

Result is 0.30 when
               Category is EDUCATION or ENTERTAINMENT or FOOD_AND_DRINK or GAME or HOUSE_AND_HOME or PHOTOGRAPHY or SHOPPING or VIDEO_PLAYERS or WEATHER
               Size < 0.0029
               Type is Free

Result is 0.38 when
               Category is AUTO_AND_VEHICLES or BUSINESS or DATING or LIBRARIES_AND_DEMO or LIFESTYLE or MEDICAL or TOOLS or TRAVEL_AND_LOCAL
               Size >= 0.0126
               Current.Ver is 0.000000091 to 0.000000127
               Type is Free

Result is 0.56 when
               Category is BOOKS_AND_REFERENCE or COMMUNICATION or HEALTH_AND_FITNESS or MAPS_AND_NAVIGATION or NEWS_AND_MAGAZINES or PERSONALIZATION or PRODUCTIVITY or SOCIAL or TOOLS or TRAVEL_AND_LOCAL
               Size < 0.0078
               Current.Ver >= 0.000000127

Result is 0.57 when
               Category is EDUCATION or ENTERTAINMENT or FOOD_AND_DRINK or GAME or HOUSE_AND_HOME or PHOTOGRAPHY or SHOPPING or VIDEO_PLAYERS or WEATHER
               Size is 0.0029 to 0.0089
               Type is Free

Result is 0.59 when
               Category is ART_AND_DESIGN or BEAUTY or BOOKS_AND_REFERENCE or COMMUNICATION or FAMILY or PERSONALIZATION or SPORTS
               Size is 0.0126 to 0.1269
               Current.Ver < 0.000000127
               Type is Free

Result is 0.72 when
               Category is AUTO_AND_VEHICLES or BOOKS_AND_REFERENCE or BUSINESS or COMICS or COMMUNICATION or DATING or FAMILY or HEALTH_AND_FITNESS or LIFESTYLE or MAPS_AND_NAVIGATION or NEWS_AND_MAGAZINES or PARENTING or PERSONALIZATION or PRODUCTIVITY or SOCIAL or SPORTS or TOOLS or TRAVEL_AND_LOCAL
               Size >= 0.0078
               Current.Ver >= 0.000000127
               Type is Free

Result is 0.72 when
               Category is COMICS or FINANCE or HEALTH_AND_FITNESS or MAPS_AND_NAVIGATION or NEWS_AND_MAGAZINES or PARENTING or PRODUCTIVITY or SOCIAL
               Size >= 0.0126
               Current.Ver is 0.000000091 to 0.000000127
               Type is Free

Result is 0.78 when
               Category is EDUCATION or ENTERTAINMENT or FOOD_AND_DRINK or GAME or HOUSE_AND_HOME or PHOTOGRAPHY or SHOPPING or VIDEO_PLAYERS or WEATHER
               Size >= 0.0089
               Type is Free

Result is 0.87 when
               Category is BEAUTY or FINANCE
               Size >= 0.0307
               Current.Ver >= 0.000000127

Machine Learning - Modelling - Random Forest

rm(train)
rm(test)
rm(dt)
rm(dt.pruned)
train <- training %>% select(-c("Rating", "Reviews"))
test <- testing %>% select(-c("Rating", "Reviews"))
rf <- randomForest(Result ~., data = train)
#plot(rf, main = "Error rate of random forest")
varImpPlot(rf, main = "Gini Plot - Importance of Variables")

test$pred = predict(rf, newdata = test[-6])
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  440  180
      Flop 205  622
                                             
               Accuracy : 0.7339             
                 95% CI : (0.7104, 0.7566)   
    No Information Rate : 0.5543             
    P-Value [Acc > NIR] : <0.0000000000000002
                                             
                  Kappa : 0.4595             
                                             
 Mcnemar's Test P-Value : 0.2213             
                                             
            Sensitivity : 0.6822             
            Specificity : 0.7756             
         Pos Pred Value : 0.7097             
         Neg Pred Value : 0.7521             
             Prevalence : 0.4457             
         Detection Rate : 0.3041             
   Detection Prevalence : 0.4285             
      Balanced Accuracy : 0.7289             
                                             
       'Positive' Class : Hit                
                                             
raf.u <- data.frame("Model" = "Random Forest (U)", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])
rm(cm)

Machine Learning - Modelling - Naive Bayes Classifier

rm(test)
rm(train)
rm(rf)
train <- training %>% select(-c("Rating", "Reviews"))
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing %>% select(-c("Rating", "Reviews"))
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

delays.nb <- naiveBayes(Result~., data = train)
delays.nb

Naive Bayes Classifier for Discrete Predictors

Call:
naiveBayes.default(x = X, y = Y, laplace = laplace)

A-priori probabilities:
Y
     Hit     Flop 
0.446037 0.553963 

Conditional probabilities:
      Size
Y            [,1]       [,2]
  Hit  0.03556935 0.06165665
  Flop 0.03563030 0.10756489

      Current.Ver
Y              [,1]       [,2]
  Hit  0.0009565182 0.02834286
  Flop 0.0017979409 0.03567101

      Category
Y      ART_AND_DESIGN AUTO_AND_VEHICLES       BEAUTY
  Hit    0.0050329075      0.0061943477 0.0034843206
  Flop   0.0093516209      0.0127805486 0.0065461347
      Category
Y      BOOKS_AND_REFERENCE     BUSINESS       COMICS
  Hit         0.0154858691 0.0158730159 0.0034843206
  Flop        0.0233790524 0.0417705736 0.0084164589
      Category
Y      COMMUNICATION       DATING    EDUCATION
  Hit   0.0313588850 0.0174216028 0.0174216028
  Flop  0.0249376559 0.0224438903 0.0068578554
      Category
Y      ENTERTAINMENT       EVENTS       FAMILY
  Hit   0.0178087495 0.0007742935 0.2040263260
  Flop  0.0028054863 0.0068578554 0.2319201995
      Category
Y           FINANCE FOOD_AND_DRINK         GAME
  Hit  0.0240030972   0.0147115757 0.1966705381
  Flop 0.0464463840   0.0102867830 0.0692019950
      Category
Y      HEALTH_AND_FITNESS HOUSE_AND_HOME
  Hit        0.0282617112   0.0096786682
  Flop       0.0246259352   0.0046758105
      Category
Y      LIBRARIES_AND_DEMO    LIFESTYLE
  Hit        0.0054200542 0.0247773906
  Flop       0.0112219451 0.0458229426
      Category
Y      MAPS_AND_NAVIGATION      MEDICAL
  Hit         0.0116144019 0.0081300813
  Flop        0.0130922693 0.0620324190
      Category
Y      NEWS_AND_MAGAZINES    PARENTING
  Hit        0.0197444832 0.0038714673
  Flop       0.0240024938 0.0059226933
      Category
Y      PERSONALIZATION  PHOTOGRAPHY PRODUCTIVITY
  Hit     0.0356174990 0.0460704607 0.0321331785
  Flop    0.0386533666 0.0174563591 0.0308603491
      Category
Y          SHOPPING       SOCIAL       SPORTS
  Hit  0.0329074719 0.0228416570 0.0301974448
  Flop 0.0124688279 0.0208852868 0.0317955112
      Category
Y             TOOLS TRAVEL_AND_LOCAL VIDEO_PLAYERS
  Hit  0.0662020906     0.0205187766  0.0185830430
  Flop 0.0950748130     0.0202618454  0.0124688279
      Category
Y           WEATHER
  Hit  0.0096786682
  Flop 0.0046758105

      Type
Y                0        Free         NaN
  Hit  0.000000000 0.992257065 0.000000000
  Flop 0.000000000 0.875311721 0.000000000
      Type
Y             Paid
  Hit  0.007742935
  Flop 0.124688279

      Content.Rating
Y            Adults     Everyone       Mature
  Hit  0.0003871467 0.8033294619 0.0514905149
  Flop 0.0000000000 0.8753117207 0.0405236908
      Content.Rating
Y              Teen      Unrated
  Hit  0.1447928765 0.0000000000
  Flop 0.0838528678 0.0003117207
pred.prob <- as.data.frame(predict(delays.nb, newdata = test, type = "raw"))
pred.prob <-  round(pred.prob, 4)
pred.prob <- pred.prob %>% select("Hit")
names(pred.prob) <-  "predraw"
test <- cbind(test, pred.prob)
rm(pred.prob)

par <- data.frame("Cut off" = 1, "Accuracy" = 1, "Sensitivity" = 1)
i = 0
j = 1
for(i in seq(0,1,0.001)){
  test <- test %>% mutate(pred = factor(as.character(ifelse(predraw < i, "Flop", "Hit")), levels = c("Hit", "Flop")))
 par[j,2] <- 1-mean(test$Result != test$pred)
 test1 <- test %>% filter(Result == "Hit")
 par[j,3] <- mean(test1$Result == test1$pred)
 par[j,1] <- i
j <- j+1
i <- i+1
rm(test1)
}
rm(i)
rm(j)

kable(par %>% filter(Accuracy == max(Accuracy)) %>% head(5), caption = "The (Hit) probability cut off for the best accuracy")

Cut.off Accuracy Sensitivity
0.615 0.6848652 0.6852713
0.616 0.6848652 0.6775194


par %>% ggplot(aes(x = `Cut.off`, y = par$Accuracy))+geom_point(size = 0.05, color = "gray")+geom_line()+geom_hline(yintercept = max(par$Accuracy), color = "red")+geom_vline(xintercept = 0.615, color = "red")+
  labs(x = "Cut Off value", y = "Accuracy of prediction", title = "Cut off Vs Accuracy")


rm(par)

test <- test %>% mutate(pred =  factor(as.character(ifelse(predraw < 0.615, "Flop", "Hit")), levels = c("Hit", "Flop")))

cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  442  253
      Flop 203  549
                                              
               Accuracy : 0.6849              
                 95% CI : (0.6602, 0.7088)    
    No Information Rate : 0.5543              
    P-Value [Acc > NIR] : < 0.0000000000000002
                                              
                  Kappa : 0.367               
                                              
 Mcnemar's Test P-Value : 0.02175             
                                              
            Sensitivity : 0.6853              
            Specificity : 0.6845              
         Pos Pred Value : 0.6360              
         Neg Pred Value : 0.7301              
             Prevalence : 0.4457              
         Detection Rate : 0.3055              
   Detection Prevalence : 0.4803              
      Balanced Accuracy : 0.6849              
                                              
       'Positive' Class : Hit                 
                                              
nab.u <- data.frame("Model" = "Naive Bayes (U)", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])
rm(cm)

Machine Learning - Modelling - SVM

rm(test)
rm(train)
rm(delays.nb)

train <- training %>% select(-c("Rating", "Reviews"))
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing %>% select(-c("Rating", "Reviews"))
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

set.seed(123)
svm.fit <- svm(Result~., data=train, kernel = "linear")

test$pred <- predict(svm.fit, newdata = test[,-6], type = "class")
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  277  138
      Flop 368  664
                                               
               Accuracy : 0.6503               
                 95% CI : (0.6251, 0.6749)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : 0.0000000000000681   
                                               
                  Kappa : 0.2667               
                                               
 Mcnemar's Test P-Value : < 0.00000000000000022
                                               
            Sensitivity : 0.4295               
            Specificity : 0.8279               
         Pos Pred Value : 0.6675               
         Neg Pred Value : 0.6434               
             Prevalence : 0.4457               
         Detection Rate : 0.1914               
   Detection Prevalence : 0.2868               
      Balanced Accuracy : 0.6287               
                                               
       'Positive' Class : Hit                  
                                               
linear <- data.frame("MOdel" = "SVM - Linear (U)", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])

rm(test)
rm(train)
rm(svm.fit)
rm(cm)

train <- training %>% select(-c("Rating", "Reviews"))
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing %>% select(-c("Rating", "Reviews"))
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

set.seed(123)
svm.fit <- svm(Result~., data=train, kernel = "radial")

test$pred <- predict(svm.fit, newdata = test[,-6], type = "class")
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit  294  130
      Flop 351  672
                                               
               Accuracy : 0.6676               
                 95% CI : (0.6426, 0.6918)     
    No Information Rate : 0.5543               
    P-Value [Acc > NIR] : < 0.00000000000000022
                                               
                  Kappa : 0.3039               
                                               
 Mcnemar's Test P-Value : < 0.00000000000000022
                                               
            Sensitivity : 0.4558               
            Specificity : 0.8379               
         Pos Pred Value : 0.6934               
         Neg Pred Value : 0.6569               
             Prevalence : 0.4457               
         Detection Rate : 0.2032               
   Detection Prevalence : 0.2930               
      Balanced Accuracy : 0.6469               
                                               
       'Positive' Class : Hit                  
                                               
radial <- data.frame("MOdel" = "SVM - Radial (U)", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])

rm(test)
rm(train)
rm(svm.fit)
rm(cm)

train <- training %>% select(-c("Rating", "Reviews"))
train$Result <- factor(train$Result, levels = c("Hit", "Flop"))
test <- testing %>% select(-c("Rating", "Reviews"))
test$Result <- factor(test$Result, levels = c("Hit", "Flop"))

set.seed(123)
svm.fit <- svm(Result~., data=train, kernel = "polynomial")

test$pred <- predict(svm.fit, newdata = test[,-6], type = "class")
cm <- confusionMatrix(factor(test$pred, levels = c("Hit", "Flop")), factor(test$Result, levels = c("Hit", "Flop")))
cm
Confusion Matrix and Statistics

          Reference
Prediction Hit Flop
      Hit    0    0
      Flop 645  802
                                             
               Accuracy : 0.5543             
                 95% CI : (0.5282, 0.5801)   
    No Information Rate : 0.5543             
    P-Value [Acc > NIR] : 0.5109             
                                             
                  Kappa : 0                  
                                             
 Mcnemar's Test P-Value : <0.0000000000000002
                                             
            Sensitivity : 0.0000             
            Specificity : 1.0000             
         Pos Pred Value :    NaN             
         Neg Pred Value : 0.5543             
             Prevalence : 0.4457             
         Detection Rate : 0.0000             
   Detection Prevalence : 0.0000             
      Balanced Accuracy : 0.5000             
                                             
       'Positive' Class : Hit                
                                             
polynomial <- data.frame("MOdel" = "SVM - Polynomial (U)", "Accuracy" = cm[["overall"]][["Accuracy"]], "Sensitivity" = cm[["byClass"]][["Sensitivity"]], "Specificity" = cm[["byClass"]][["Specificity"]])

rm(cm)
rm(test)
rm(train)
rm(svm.fit)

svm.u <- rbind(linear, radial, polynomial)
rm(linear)
rm(radial)
rm(polynomial)
names(svm.u) <- c("Model", "Accuracy", "Sensitivity", "Specificity")

Final model Summary

model.ds <- rbind(logit, det, raf, nab, svm)
rm(logit)
rm(det)
rm(raf)
rm(nab)
rm(svm)
model.realtime <- rbind(logit.u, det.u, raf.u, nab.u, svm.u)
rm(logit.u)
rm(det.u)
rm(raf.u)
rm(nab.u)
rm(svm.u)

kable(rbind(model.ds, model.realtime), caption = "Final model results for complete and real time data")

Model Accuracy Sensitivity Specificity
Logistic Regression 0.9474775 0.9162791 0.9725686
Decision Trees 0.9447132 0.9193798 0.9650873
Random Forest 0.9474775 0.9503876 0.9451372
Naive Bayes 0.9246717 0.8573643 0.9788030
SVM - Linear 0.7864547 0.5829457 0.9501247
SVM - Radial 0.7270214 0.5317829 0.8840399
SVM - Polynomial 0.5694540 0.0341085 1.0000000
Logistic Regression (U) 0.6862474 0.6961240 0.6783042
Decision Tress (U) 0.6973048 0.7255814 0.6745636
Random Forest (U) 0.7339323 0.6821705 0.7755611
Naive Bayes (U) 0.6848652 0.6852713 0.6845387
SVM - Linear (U) 0.6503110 0.4294574 0.8279302
SVM - Radial (U) 0.6675881 0.4558140 0.8379052
SVM - Polynomial (U) 0.5542502 0.0000000 1.0000000


kable((model.ds %>% arrange(desc(Accuracy)) %>% head(2)), caption = "Top 2 models for complete data")

Model Accuracy Sensitivity Specificity
Logistic Regression 0.9474775 0.9162791 0.9725686
Random Forest 0.9474775 0.9503876 0.9451372


kable((model.realtime%>% arrange(desc(Accuracy)) %>% head(2)), caption = "Top 2 models for data that can be extracted in real time")
Model Accuracy Sensitivity Specificity
Random Forest (U) 0.7339323 0.6821705 0.7755611
Decision Tress (U) 0.6973048 0.7255814 0.6745636

Conclusion:

It is clear that the size of the app and category define if the app would be a hit or not.
From the EDA it is clear that the app must be below 25 MB and the categories to target would vary depending on the risk one wants to take.
While Game, Family, and Communication give a constant reward, the highest can be gained from Categories that are yet to be explored like Comics (Which are positively seen but are low in market) or Events (Which have a huge criticism and are low in market).

Sources:
https://www.kaggle.com/lava18/google-play-store-apps
http://r-statistics.co/
https://uc-r.github.io/
https://www.r-bloggers.com/
https://medium.com/analytics-vidhya/a-guide-to-machine-learning-in-r-for-beginners-part-5-4c00f2366b90
https://en.proft.me/2016/11/9/classification-using-decis ion-trees-r/
https://www.gormanalysis.com/blog/decision-trees-in-r-us ing-rpart/
http://www.sthda.com/english/articles/35-statistical-machine-learning-essentials/141-cart-model-decision-tree-essentials/
https://dataaspirant.com/2017/02/03/decision-tree-classifier-implementation-in-r/
https://www.blopig.com/blog/2017/04/a-very-basic-introduction-to-random-forests-using-r/
https://www.rdocumentation.org/
https://www.datacamp.com/

Introduction to Data Mining
Book by Michael Steinbach, Pang-Ning Tan, and Vipin Kumar

Data Mining for Business Analytics: Concepts, Techniques, and Applications in R
Book by Galit Shmueli , Peter C. Bruce, Inbal Yahav, Nitin R. Patel, Kenneth C. Lichtendahl Jr. 

LS0tDQp0aXRsZTogIlN1c2hhbnRoIENoaW50YWxhcGF0aSBBZHZhbmNlZCBCQSB3aXRoIFIgcHJvamVjdCINCm91dHB1dDogaHRtbF9ub3RlYm9vaw0KZWRpdG9yX29wdGlvbnM6IA0KICBjaHVua19vdXRwdXRfdHlwZTogaW5saW5lDQotLS0NCg0KYGBge3IgcGFja2FnZXMgaW5zdGFsbGF0aW9uLCBpbmNsdWRlPUZBTFNFLCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCmlmKCFyZXF1aXJlKCJwYWNtYW4iKSkgaW5zdGFsbC5wYWNrYWdlcygicGFjbWFuIikNCnBhY21hbjo6cF9sb2FkKHRpZHl2ZXJzZSwgZHBseXIsIGdncGxvdDIsIGdnRXh0cmEsIGtuaXRyLCB2aXJpZGlzLCBjb3JycGxvdCwgd29yZGNsb3VkMiwgdG0sIHRleHRjYXQsIHJwYXJ0LCBycGFydC5wbG90LCByYW5kb21Gb3Jlc3QsIGUxMDcxLCBjYXJldCkNCmBgYA0KDQpgYGB7ciBMb2FkaW5nIExpYnJhcmllcywgaW5jbHVkZT1GQUxTRSwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpsaWJyYXJ5KCJ0aWR5dmVyc2UiKQ0KbGlicmFyeSgiZHBseXIiKQ0KbGlicmFyeSgiZ2dwbG90MiIpDQpsaWJyYXJ5KCJnZ0V4dHJhIikNCmxpYnJhcnkoImtuaXRyIikNCmxpYnJhcnkoInZpcmlkaXMiKQ0KbGlicmFyeSgiY29ycnBsb3QiKQ0KbGlicmFyeSgiZTEwNzEiKQ0KbGlicmFyeSgiY2FyZXQiKQ0KbGlicmFyeSgid29yZGNsb3VkMiIpDQpsaWJyYXJ5KCJ0bSIpDQpsaWJyYXJ5KCJ0ZXh0Y2F0IikNCmxpYnJhcnkoInJwYXJ0IikNCmxpYnJhcnkoInJwYXJ0LnBsb3QiKQ0KbGlicmFyeSgicmFuZG9tRm9yZXN0IikNCm9wdGlvbnMoc2NpcGVuID0gOTk5KQ0KYGBgDQoNCkxvYWRpbmcgaW4gdGhlIGRhdGENCmBgYHtyIExvYWRpbmcgRGF0YSwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpncHMgPC0gcmVhZC5jc3YoImdvb2dsZXBsYXlzdG9yZS5jc3YiKQ0KYGBgDQoNCkRhdGEgQ2xlYW5pbmcNCg0KVGhlIGZvbGxvd2luZyBuZWVkcyB0byBiZSByZW1vdmVkLyBjbGVhbmVkOiAgDQoxKSBSZWNvcmQgb2YgYW4gQXBwIGJ5IG5hbWUgTGlmZSBtYWRlIHdpLWZpLi4uLiAgDQoyKSBUaGUgQ3VycmVudCBWZXJzaW9uIG9mIHRoZSBBcHAgYW5kIEFuZHJvaWQgVmVyc2lvbiAgIGhhdmUgdG8gYmUgcHJvY2Vzc2VkIHRvIGRpc3BsYXkgdGhlIGN1cnJlbnQgc2VxdWVuY2UgKDEyLjEuMiBpcyAxMikgIA0KMykgVGhlIFNwZWNpYWwgY2hhcmFjdGVycyBpbiBJbnN0YWxscygrKSwgUHJpY2UoJCksIFNpemUoTUIpIHNob3VsZCBiZSByZW1vdmVkICANCjQpIEFsbCB0aGUgcmVkdW5kYW50IHJlY29yZHMgc2hvdWxkIGJlIHJlbW92ZWQgIA0KNSkgVGhlIGxldmVscyBpbiBJbnN0YWxscyBzaG91bGQgYmUgZml4ZWQgdG8gYSBEZXNjZW5kaW5nIG9yZGVyICANCjYpIEdlbmVycyBzaG91bGQgYmUgcmVtb3ZlZCwgaXQgaXMgdGhlIHNhbWUgYXMgQ2F0ZWdvcnkgYW5kIG1ha2VzIGl0IHJlZHVuZGFudCAgDQo3KSBBbGwgdGhlIG90aGVyIE5hcyBzaG91bGQgYmUgcmVtb3ZlZCAgDQo4KSBGaXhpbmcgQ29udGVudCBSYXRpbmcgYnkgY29tYmluaW5nIGxpa2UgbGV2ZWxzICANCmBgYHtyIENsZWFuaW5nIERhdGEsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0KZ3BzIDwtIGdwc1tncHMkQXBwICE9CSJMaWZlIE1hZGUgV0ktRmkgVG91Y2hzY3JlZW4gUGhvdG8gRnJhbWUiLF0gJT4lIG5hLm9taXQoKSAlPiUgdW5pcXVlKCkgJT4lIHNlbGVjdCgtR2VucmVzKQ0KZ3BzJEN1cnJlbnQuVmVyIDwtIGdwcyRDdXJyZW50LlZlciAlPiUgYXMuY2hhcmFjdGVyKCkNCmkgPC0gMQ0KZm9yKGkgaW4gYyhzZXEoMSwgODg5MiwgYnkgPSAxKSkpew0KICBhIDwtIGdwcyRDdXJyZW50LlZlcltpXSANCiAgYiA8LSBiIDwtIHVubGlzdChzdHJzcGxpdChhLCAiWy5dIikpDQogIGIgPC0gYlsxXQ0KICBncHMkQ3VycmVudC5WZXJbaV0gPSBiDQogIHJtKGIpDQogIHJtKGEpDQogIGkgPC0gaSArIDENCn0NCnJtKGkpDQpncHMkQW5kcm9pZC5WZXIgPC0gZ3BzJEFuZHJvaWQuVmVyICU+JSBhcy5jaGFyYWN0ZXIoKQ0KaSA8LSAxDQpmb3IoaSBpbiBjKHNlcSgxLCA4ODkyLCBieSA9IDEpKSl7DQogIGEgPC0gZ3BzJEFuZHJvaWQuVmVyW2ldDQogIGIgPC0gYiA8LSB1bmxpc3Qoc3Ryc3BsaXQoYSwgIlsuXSIpKQ0KICBiIDwtIGJbMV0NCiAgZ3BzJEFuZHJvaWQuVmVyW2ldIDwtIGINCiAgcm0oYikNCiAgcm0oYSkNCiAgaSA8LSBpICsgMQ0KfQ0Kcm0oaSkNCg0KZ3BzJENhdGVnb3J5IDwtIGdwcyRDYXRlZ29yeSAlPiUgZHJvcGxldmVscygpDQpncHMkSW5zdGFsbHMgPSBnc3ViKCJbXjAtOS5dIiwgJycsIChncHMkSW5zdGFsbHMgJT4lIGFzLmNoYXJhY3RlcigpKSkNCiAgICBncHMkSW5zdGFsbHMgPSBhcy5udW1lcmljKGdwcyRJbnN0YWxscykNCmdwcyRTaXplID0gZ3N1YigiW14wLTkuXSIsICcnLCAoZ3BzJFNpemUgJT4lIGFzLmNoYXJhY3RlcigpKSkNCiAgICBncHMkU2l6ZSA9IGFzLm51bWVyaWMoZ3BzJFNpemUpDQpncHMkUHJpY2UgPSBnc3ViKCJbXjAtOS5dIiwgJycsIChncHMkUHJpY2UgJT4lIGFzLmNoYXJhY3RlcigpKSkNCiAgICBncHMkUHJpY2UgPSBhcy5udW1lcmljKGdwcyRQcmljZSkgICAgDQoNCmdwcyRJbnN0YWxscyA8LSBmYWN0b3IoZ3BzJEluc3RhbGxzLCBsZXZlbHMgPSBjKCIxMDAwMDAwMDAwIiwgIjUwMDAwMDAwMCIsICIxMDAwMDAwMDAiLCAiNTAwMDAwMDAiLCAiMTAwMDAwMDAiLCAiNTAwMDAwMCIsICIxMDAwMDAwIiwgIjUwMDAwMCIsICIxMDAwMDAiLCAiNTAwMDAiLCAiMTAwMDAiLCAiNTAwMCIsICIxMDAwIiwgIjUwMCIsICIxMDAiLCAiNTAiLCAiMTAiLCAiNSIsICIxIikpDQoNCmdwcyRDb250ZW50LlJhdGluZyA8LSBncHMkQ29udGVudC5SYXRpbmcgJT4lIGFzLmNoYXJhY3RlcigpICU+JSB3b3JkKDEpICU+JSBhcy5mYWN0b3IoKSAlPiUgZHJvcGxldmVscygpDQoNCmBgYA0KDQoNCkRpZmZlcmVudCBDYXRlZ29yaWVzIGFuZCB0aGVpciBwcmVzZW5jZSBpbiB0aGUgbWFya2V0IA0KDQpPZiB0aGUgZGlmZmVyZW50IGtpbmRzIG9mIHBlb3BsZSBwcmVzZW50IGluIHRoaXMgd29yZCwgV2hpY2ggY2F0ZWdvcnkgaXMgZ2xvYmFsbHkgYWNjZXB0ZWQ/ICANCldoYXQgYXJlIHRoZSB0b3AgY2F0ZWdvcmllcyB0aGF0IHBlcmZvcm0gdGhlIGJlc3Q/ICANCg0KYGBge3IgRURBIC0gQmVzdCBDYXRlZ29yaWVzIGFzIHBlciBSYXRpbmcsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0KZ3BzX2NiIDwtIGdwcyAlPiUgbmEub21pdCgpICU+JSBncm91cF9ieShDYXRlZ29yeSkgJT4lIHN1bW1hcml6ZShTdW0gPSBzdW0oUmF0aW5nKSwgY291bnQgPSBuKCksIGBOb3JtYWxpemVkIFJhdGluZ2AgPSBzdW0oUmF0aW5nKS9uKCkpICU+JSBhcnJhbmdlKGRlc2MoYE5vcm1hbGl6ZWQgUmF0aW5nYCkpDQprYWJsZShoZWFkKGdwc19jYiwgNyksIGNhcHRpb24gPSAiVG9wIDcgY2F0ZWdvcmllcyBiYXNlZCBvbiBOb3JtYWxpemVkIFJhdGluZyIpDQprYWJsZSh0YWlsKGdwc19jYiwgNyksIGNhcHRpb24gPSAiQm90dG9tIDcgY2F0ZWdvcmllcyBiYXNlZCBvbiBOb3JtYWxpemVkIFJhdGluZyIpDQogIA0KZ3BzICU+JSBmaWx0ZXIoQ2F0ZWdvcnkgJWluJSAoZ3BzX2NiJENhdGVnb3J5ICU+JSBoZWFkKDE1KSkpICU+JSBnZ3Bsb3QoYWVzKHggPSBDYXRlZ29yeSwgeSA9IFJhdGluZywgZmlsbCA9IENhdGVnb3J5KSkgKyBnZW9tX3Zpb2xpbihjb2xvciA9ICJibGFjayIpICsgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKSArIGdlb21faGxpbmUoc2l6ZSA9IDAuNiwgY29sb3IgPSAicmVkIiwgbGluZXR5cGUgPSAnZGFzaGVkJywgYWxwaGEgPSAwLjUsIHlpbnRlcmNlcHQgPSBtZWFuKGdwcyRSYXRpbmcgJT4lIG5hLm9taXQoKSkpK2Nvb3JkX2ZsaXAoKStsYWJzKHkgPSAiUmF0aW5nIiwgeCA9ICJDYXRlZ29yeSIsIHRpdGxlID0gIkNhdGVnb3J5IFZzIFJhdGluZyBWaW9saW4gUGxvdCIpDQpybShncHNfY2IpDQpgYGANCg0KRXZlbnRzLCBFZHVjYXRpb24gYW5kIFBlcnNvbmFsaXphdGlvbiBhcmUgdGhlIGJlc3QgaW4gb3ZlcmFsbCByYXRpbmcuICANCldoaWxlIERhdGluZywgVG9vbHMsIE1hcHMgYW5kIE5hdmlnYXRpb24gYXJlIHRoZSB3b3JzdCByYXRlZCBhcHAgY2F0ZWdvcmllcy4gICANCg0KU3VidGxlIGFuZCBTaWduaWZpY2FudCBDYXRlZ29yeSBGb3JlbnNpY3MNCg0KV2hpY2ggY2F0ZWdvcnkgZG9lc24ndCBwZXJmb3JtIHdlbGwgb24gYSBvdmVyYWxsIHNjYWxlPyAgDQpXaGF0IGFyZSB0aGUgdG90YWwgbnVtYmVyIG9mIGluc3RhbGxzIGZvciBkaWZmZXJlbnQgY2F0ZWdvcmllcz8gIA0KDQpgYGB7ciBFREEgLSBDYXRlZ29yeSBGb3JlbnNpY3MsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0KZGF0YSA8LSBncHMgJT4lIGdyb3VwX2J5KENhdGVnb3J5KSAlPiUgc3VtbWFyaXNlKENvdW50ID0gbigpLCBgVG90YWwgaW5zdGFsbHNgID0gc3VtKGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKEluc3RhbGxzKSkpLCBgTm9ybWFsaXplZCBJbnN0YWxsYXRpb25zYCA9IGBUb3RhbCBpbnN0YWxsc2AvQ291bnQpIA0KDQpkYXRhICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gcmVvcmRlcihDYXRlZ29yeSwgQ291bnQpLCBmaWxsID0gQ2F0ZWdvcnksIHkgPSBDb3VudCkpK2dlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBjb2xvciA9ICJibGFjayIpK3RoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikrY29vcmRfZmxpcCgpKw0KICBsYWJzKHkgPSAiTnVtYmVyIG9mIEFwcHMgcHJlc2VudCIsIHggPSAiQ2F0ZWdvcnkiLCB0aXRsZSA9ICJOdW1iZXIgb2YgQXBwcyBwcmVzZW50IGluIERpZmZlcmVudCBDYXRlZ29yeSIpDQoNCmRhdGEgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSByZW9yZGVyKENhdGVnb3J5LCBgVG90YWwgaW5zdGFsbHNgKSwgZmlsbCA9IENhdGVnb3J5LCB5ID0gYFRvdGFsIGluc3RhbGxzYCkpK2dlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiLCBjb2xvciA9ICJibGFjayIpK3RoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikrY29vcmRfZmxpcCgpKw0KICBsYWJzKHkgPSAiVG90YWwgbnVtYmVyIG9mIGluc3RhbGxzIiwgeCA9ICJDYXRlZ29yeSIsIHRpdGxlID0gIlRvdGFsIE51bWJlciBvZiBJbnN0YWxsYXRpb25zIHBlciBDYXRlZ29yeSIpDQoNCmRhdGEgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSByZW9yZGVyKENhdGVnb3J5LCBgTm9ybWFsaXplZCBJbnN0YWxsYXRpb25zYCksIGZpbGwgPSBDYXRlZ29yeSwgeSA9IGBOb3JtYWxpemVkIEluc3RhbGxhdGlvbnNgKSkrZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIsIGNvbG9yID0gImJsYWNrIikrdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKStjb29yZF9mbGlwKCkrDQogIGxhYnMoeSA9ICJUb3RhbCBudW1iZXIgb2Ygbm9ybWFsaXplZCBpbnN0YWxsYXRpb25zIiwgeCA9ICJDYXRlZ29yeSIsIHRpdGxlID0gIlRvdGFsIE51bWJlciBvZiBOb3JtYWxpemVkIEluc3RhbGxhdGlvbnMgcGVyIENhdGVnb3J5IikNCg0KaW5mbyA8LSBjYmluZCgoZGF0YSAlPiUgYXJyYW5nZShkZXNjKGRhdGEkQ291bnQpKSAlPiUgc2VsZWN0KENhdGVnb3J5KSksDQooZGF0YSAlPiUgYXJyYW5nZShkZXNjKGRhdGEkYFRvdGFsIGluc3RhbGxzYCkpICU+JSBzZWxlY3QoQ2F0ZWdvcnkpKSwNCihkYXRhICU+JSBhcnJhbmdlKGRlc2MoZGF0YSRgTm9ybWFsaXplZCBJbnN0YWxsYXRpb25zYCkpICU+JSBzZWxlY3QoQ2F0ZWdvcnkpKSkNCm5hbWVzKGluZm8pIDwtIGMoIkJ5IENvdW50IiwgIkJ5IFRvdGFsIEluc3RhbGxzIiwgIkJ5IFRvdGFsIE5vcm1hbGl6ZWQgSW5zdGFsbHMiKQ0Ka2FibGUoaGVhZChpbmZvLCA3KSwgY2FwdGlvbiA9ICJUb3AgNyBjYXRlZ29yaWVzIikNCmthYmxlKHRhaWwoaW5mbywgNyksIGNhcHRpb24gPSAiQm90dG9tIDcgY2F0ZWdvcmllcyIpDQpybShpbmZvKQ0KYGBgDQpGYW1pbHkgaGFzIHRoZSBoaWdoZXN0IGNvbXBldGl0aW9uIChudW1iZXIgb2YgYXBwcykgZm9sbG93ZWQgYnkgdG9vbHMgd2hpY2ggZG9uJ3Qgc2VlbSB0byBiZSByZXdhcmRpbmcuICAgDQpHYW1lLCBhbmQgUHJvZHVjdGl2aXR5IGRvIHdlbGwgd3J0IHRvIG51bWJlciBvZiBpbnN0YWxscyBldmVuIHRob3VnaCB0aGV5IGhhdmUgYSBsb3Qgb2YgY29tcGV0aXRpb24uICANCkNvbW11bmljYXRpb24gYW5kIFNvY2lhbCBhcmUgdHdvIGRvbWFpbnMgd2hlcmUgdGhlcmUgaXMgYSBzdXJnZSBpbiBkb3dubG9hZHMgYW5kIHRoZSBudW1iZXIgb2YgY29tcGV0aXRvcnMgYXJlIGFsc28gbGVzcy4gIA0KDQpDb21tZW50cywgUGFyZW50aW5nLCBhbmQgRXZlbnRzIGNhbiBiZSBzZWVuIGFzIGRvbWFpbnMgdGhhdCBvbmUgbXVzdCBpZ25vcmUgYXMgdGhlcmUgYXJlIHZlcnkgZmV3IGRvd25sb2FkcyBhbmQgc28gaXMgdGhlIGNvbXBldGl0aW9uLiBPciB0aGV5IGNhbiBhbHNvIGJlIHNlZW4gYXMgZG9tYWlucyB0aGF0IGhhdmUgaHVnZSBwb3RlbnRpYWwgYW5kIGFyZSB1bmRlcmRvZyBtYXJrZXRzIHRoYXQgY2FuIGJlIGV4cGxvaXRlZCB3aXRoIGEgZGlzcnVwdGl2ZSBpZGVhLiAgDQogDQoNCg0KQ29udGVudCBUeXBlIA0KDQpXaGF0IGlzIHRoZSBkaXN0cmlidXRpb24gb2YgYXBwcyBvZiB2YXJpb3VzIGNvbnRlbnQ/ICANCklzIHRoZXJlIGFuIGVmZmVjdCBvZiBjb250ZW50IHR5cGUgb2YgaW5zdGFsbHM/ICANCkhvdyBhcmUgdGhleSBwcmljZWQ/ICANCmBgYHtyIEVEQSAtICBDb250ZW50IFR5cGUgYW5kIENvdW50LCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCmdwcyAlPiUgZ3JvdXBfYnkoQ29udGVudC5SYXRpbmcpICU+JSBzdW1tYXJpc2UoY291bnQgPSBuKCkpICU+JQ0KICBnZ3Bsb3QoYWVzKHggPSByZW9yZGVyKENvbnRlbnQuUmF0aW5nLCAtY291bnQpLCB5ID0gY291bnQsIGZpbGwgPSBDb250ZW50LlJhdGluZykpKyBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IiwgY29sb3IgPSAiYmxhY2siKSsNCiBzY2FsZV9maWxsX3ZpcmlkaXMoZGlzY3JldGUgPSBUUlVFKSArDQogIGxhYnMoeSA9ICJDb3VudCIsIHggPSAiQ29udGVudCBUeXBlIiwgdGl0bGUgPSAiQ29udGVudCB0eXBlIGFuZCB0aGVpciBwcmVzZW5jZSIpDQpgYGANCkFwcHMgZm9yIGV2ZXJ5b25lIGFyZSBwcmVzZW50IGluIGJ1bGsgIA0KDQpJbnN0YWxscyB2cyBSYXRpbmcgcGVyIGNvbnRlbnQgdHlwZQ0KYGBge3IgRURBIC0gIENvbnRlbnQgdHlwZS0gSW5zdGFsbHMgYW5kIFJhdGluZywgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpncHMgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBSYXRpbmcsIHkgPSBhcy5pbnRlZ2VyKGFzLmNoYXJhY3RlcihncHMkSW5zdGFsbHMpKSwgY29sb3IgPSBDb250ZW50LlJhdGluZykpKw0KICBnZW9tX3BvaW50KCkrDQogIGZhY2V0X3dyYXAofkNvbnRlbnQuUmF0aW5nKSArIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDUgLGNvbG91ciA9ICdyZWQnLCBsaW5ldHlwZSA9ICdkYXNoZWQnKSArIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDQgLGNvbG91ciA9ICdyZWQnLCBsaW5ldHlwZSA9ICdkYXNoZWQnKSArICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSAxMDAxMDAwMDAwICxjb2xvdXIgPSAncmVkJywgbGluZXR5cGUgPSAnZGFzaGVkJykgKyBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSA3NTEwMDAwMDAgLGNvbG91ciA9ICdyZWQnLCBsaW5ldHlwZSA9ICdkYXNoZWQnKQ0KYGBgDQpFdmVyeW9uZSBhbmQgVGVlbnMgY29udGVudCB0eXBlIGF0dHJhY3QgdGhlIG1vc3QgbnVtYmVyIG9mIGluc3RhbGxzIGFuZCBhIGRlY2VudCByYXRpbmcuICANCg0KDQpSYXRpbmcgYW5hbHlzaXMgIA0KDQpBcHAgcmF0aW5ncyAob24gYSBzY2FsZSBvZiAxIHRvIDUpIGltcGFjdCB0aGUgZGlzY292ZXJhYmlsaXR5LCBhbmQgaW5kaWNhdGVzIHRoZSBjb21wYW55J3Mgb3ZlcmFsbCBicmFuZCBpbWFnZS4gUmF0aW5ncyBhcmUgYSBrZXkgcGVyZm9ybWFuY2UgaW5kaWNhdG9yIG9mIGFuIGFwcC4NCmBgYHtyIEVEQSAtICBSYXRpbmcgYW5hbHlzaXMgLSBEaXN0cmlidXRpb24sIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0KZ3BzICAlPiUgZ2dwbG90KGFlcyh4ID0gUmF0aW5nKSkrZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSAwLjEsIGZpbGwgPSAicHVycGxlIikrZ2VvbV92bGluZShjb2xvciA9ICJyZWQiLCBsaW5ldHlwZSA9ICdkYXNoZWQnLCB4aW50ZXJjZXB0ID0gbWVhbihncHMkUmF0aW5nICU+JSBuYS5vbWl0KCkpKSt0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpK2xhYnMoeSA9ICJDb3VudCIsIHggPSAiUmF0aW5nIiwgdGl0bGUgPSAiUmF0aW5nIEhpc3RvZ3JhbSIpDQpgYGANCk1vc3Qgb2YgdGhlIGFwcHMgaGF2ZSBhbiBhdmVyYWdlIHJhdGluZyBhYm92ZSA0IGR1ZSB0byB3aGljaCB0aGVyZSBpcyBhIHNpZ25pZmljYW50IHNrZXduZXNzIHByZXNlbnQgaW4gdGhlIHJhdGluZyBwYXJ0LiAgIA0KUmF0aW5ncyBhcmUgcmlnaHQgc2tld2VkIHdpdGggbWFqb3IgYXBwcyBiZWluZyBoaWdobHkgcmF0ZWQgYW5kIG9ubHkgZmV3IHdpdGggYSBsb3cgcmF0aW5nLiAgDQoNClRoaXMgaW5kaWNhdGVzIHRoYXQgbW9zdCBvZiB0aGUgYXBwcyBpbiB0aGUgTWFya2V0IGFyZSBzaXplZCBiZWxvdyAyNW1iLiAgDQoNClRoZSBhcHBsaWNhdGlvbnMgd2l0aCBsYXJnZSBzaXplIGNhbiBtYWtlIGl0IGRpZmZpY3VsdCBmb3IgdXNlcnMgdG8gZG93bmxvYWQuIEhpZ2hlciBkb3dubG9hZCBkdXJhdGlvbnMgY291bGQgdXBzZXQgdGhlIHVzZXIgYW5kIGlnbm9yZSB0aGUgYXBwLiAgIA0KTW9yZSBpbXBvcnRhbnRseSB0aGUgbWVtb3J5IGEgdXNlciBoYXMgaW4gdGhlIGRldmljZSBpcyBsaW1pdGVkLiBUaGlzIGlzIHRvIGJlIHRlc3RlZC4gIA0KTGV04oCZcyBpbnZlc3RpZ2F0ZSB0aGUgc2FtZSB3aXRoIHRoZSBkYXRhLiAgDQoNClRoZSBkaXN0cmlidXRpb24gb2Ygc2l6ZQ0KYGBge3IgRURBIC0gIFNpemUgRGlzdHJpYnV0aW9uLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0KZ3BzICU+JSBnZ3Bsb3QoYWVzKHggPSBTaXplKSkrZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSAxLCBmaWxsID0gInB1cnBsZSIpK3RoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikrbGFicyh5ID0gIkNvdW50IiwgeCA9ICJTaXplIChNQikiLCB0aXRsZSA9ICJTaXplIGRpc3RyaWJ1dGlvbiBIaXN0b2dyYW0iKQ0KDQpncHMgICU+JSBmaWx0ZXIoU2l6ZSA8IDEyMCkgJT4lIGdncGxvdChhZXMoeCA9IFNpemUpKStnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDEsIGZpbGwgPSAicHVycGxlIikrdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKStsYWJzKHkgPSAiQ291bnQiLCB4ID0gIlNpemUgKE1CKSIsIHRpdGxlID0gIlNpemUgZGlzdHJpYnV0aW9uIEhpc3RvZ3JhbSAoU2l6ZXMgYmVsb3cgMTIwTWIpIikNCmBgYA0KTW9zdCBvZiB0aGUgYXBwcyBpbiB0aGUgTWFya2V0IGFyZSBzaXplZCBiZWxvdyAyNW1iLiAgDQoNCk5leHQgaXMgdGhlIHNpemUuICANClRoZSBhcHBsaWNhdGlvbnMgd2l0aCBsYXJnZSBzaXplIGNhbiBtYWtlIGl0IGRpZmZpY3VsdCBmb3IgdXNlcnMgdG8gZG93bmxvYWQuIEhpZ2hlciBkb3dubG9hZCBkdXJhdGlvbnMgY291bGQgdXBzZXQgdGhlIHVzZXIgYW5kIGlnbm9yZSB0aGUgYXBwLiAgIA0KTW9yZSBpbXBvcnRhbnRseSB0aGUgbWVtb3J5IGEgdXNlciBoYXMgaW4gdGhlIGRldmljZSBpcyBsaW1pdGVkLiBUaGlzIGlzIHRvIGJlIHRlc3RlZC4gIA0KDQpJbXBhY3Qgb2YgU2l6ZSBvbiBSYXRpbmcNCmBgYHtyIEVEQSAtICBTaXplIFZzIFJhdGluZywgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpwIDwtIGdwcyAlPiUgZ2dwbG90KGFlcyh4ID0gU2l6ZSwgeSA9IFJhdGluZykpK2dlb21fcG9pbnQoKStsYWJzKHkgPSAiUmF0aW5nIiwgeCA9ICJTaXplIChNQikiLCB0aXRsZSA9ICJTaXplIFZzIFJhdGluZyIpIA0KZ2dNYXJnaW5hbChwLCB0eXBlID0gImhpc3RvZ3JhbSIpDQoNCg0KcCA8LSBncHMgJT4lIGZpbHRlcihTaXplIDwgMTAwKSAlPiUgZ2dwbG90KGFlcyh4ID0gU2l6ZSwgeSA9IFJhdGluZykpK2dlb21fcG9pbnQoKStsYWJzKHkgPSAiUmF0aW5nIiwgeCA9ICJTaXplIChNQikiLCB0aXRsZSA9ICJTaXplIFZzIFJhdGluZyAoU2l6ZXMgYmVsb3cgMTAwKSIpIA0KZ2dNYXJnaW5hbChwLCB0eXBlID0gImhpc3RvZ3JhbSIpDQoNCg0KDQpwIDwtIGdwcyAlPiUgZmlsdGVyKFNpemUgPCAxMCkgJT4lIGdncGxvdChhZXMoeCA9IFNpemUsIHkgPSBSYXRpbmcpKStnZW9tX3BvaW50KCkrbGFicyh5ID0gIlJhdGluZyIsIHggPSAiU2l6ZSAoTUIpIiwgdGl0bGUgPSAiU2l6ZSBWcyBSYXRpbmcgKFNpemVzIGJlbG93IDEwKSIpICANCmdnTWFyZ2luYWwocCwgdHlwZSA9ICJoaXN0b2dyYW0iKQ0Kcm0ocCkNCmBgYA0KTWFqb3JpdHkgb2YgYXBwcyB3aXRoIGhpZ2ggcmF0aW5nIChyYXRpbmcgb3ZlciA0KSBhcmUgYmV0d2VlbiAxIE1CIHRvIDEwIE1CLiAgDQoNCldoYXQgaXMgdGhlIHZhcmlhdGlvbiBpbiB0aGUgbnVtYmVyIG9mIGluc2FsbGF0aW9ucw0KYGBge3IgRURBIC0gIEluc3RhbGxzIERpc3RyaWJ1dGlvbiwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpncHMgJT4lIGdncGxvdChhZXMoeCA9IEluc3RhbGxzKSkgKw0KICBnZW9tX2hpc3RvZ3JhbShzdGF0ID0gJ2NvdW50JywgYmlucyA9IDEwMCwgZmlsbCA9ICJwdXJwbGUiLCBjb2xvciA9ICdibGFjaycpKw0KY29vcmRfZmxpcCgpICsgIHNjYWxlX3lfY29udGludW91cyhicmVha3MgPSBzZXEoMCwgMTUwMCwgMTAwKSkrbGFicyh5ID0gIkNvdW50IiwgeCA9ICJJbnN0YWxscyBSYW5nZSIsIHRpdGxlID0gIkluc3RhbGxzIGRpc3RyaWJ1dGlvbiBIaXN0b2dyYW0iKQ0KYGBgDQpUaGUgSW5zdGFsbHMgYXJlIHBlcmZlY3RseSBkaXN0cmlidXRlZCBhcyBleHBlY3RlZC4gVGhlIGhpZ2ggZW5kIG1hcmtldCBpcyB2ZXJ5IGNvbXBldGl0aXZlIGFuZCB2ZXJ5IGZldyBhcHBzIGZhbGwgaW50byB0aGF0IHNwYWNlLCB3aGlsZSBtb3N0IG9mIHRoZSBvdGhlcnMgZmFsbCBiZWhpbmQgdGhlIHJhY2UgZ2V0dGluZyBpbiBtZWRpb2NyZSBkb3dubG9hZHMuIEJ1dCB0aGUgaW50ZXJlc3RpbmcgcGFydCBpcyB0aGF0IHRoZXJlIGFyZSBhIGxvdCBvZiBhcHBzIHRoYXQgZmFsbCBpbnRvIDFNIGRvd25sb2FkcyBwb2ludC4gVGhpcyBpcyBpbnRlcmVzdGluZyBhcyB0aGlzIGdpdmVzIGEgaG9wZSB0byBhIGxvdCBvZiBkZXNpZ25lcnMgYXMgdGhlcmUgaXMgc2NvcGUuICANCg0KDQpgYGB7ciBFREEgLSAgSW5zdGFsbHMgTWFya2V0IFNlZ21lbnRhdGlvbiwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpkYXRhIDwtIGdwcyAlPiUgZ3JvdXBfYnkoSW5zdGFsbHMpICU+JSBzdW1tYXJpc2UoQ291bnQgPSBuKCkpICU+JSBhcnJhbmdlKGRlc2MoSW5zdGFsbHMpKSANCmRhdGEgPC0gZGF0YSAlPiUgbXV0YXRlKHByb2R1Y3QgPSBhcy5udW1lcmljKGFzLmNoYXJhY3RlcihkYXRhJEluc3RhbGxzKSkqZGF0YSRDb3VudCwgYG1hcmtldCBzaGFyZWAgPSBwcm9kdWN0LzE0NjYyNTk1MTMzOCoxMDAsIGBtYXJrZXQgc2hhcmUgcGVyIGFwcCBpbiB0aGUgc2VnbWVudGAgPSBgbWFya2V0IHNoYXJlYC9Db3VudCkNCm5hbWVzKGRhdGEpIDwtIGMoIkluc3RhbGxzIFJhbmdlIiwgIlRvdGFsIE51bWJlciBvZiBhcHBzIHByZXNlbnQiLCAiVG90YWwgSW5zdGFsbGF0aW9ucyIsIk1hcmtldCBzaGFyZSBvZiB0aGUgc2VnbWVudCIsICJNYXJrZXQgc2hhcmUgcGVyIGFwcCBpbiB0aGF0IHNlZ21lbnQiKQ0KDQprYWJsZShkYXRhLCBjYXB0aW9uID0gIkluc3RhbGxhdGlvbnMgYW5kIHRoZWlyIGNvbnRyaWJ1dGlvbiIpDQpgYGANClRoZSB0b3AgZml2ZSBzZWdtZW50cyBtYWtlIHVwIDk2LjM4JSBvZiB0aGUgbWFya2V0IHNoYXJlLg0KVGhpcyBpbXBsaWVzIHRoYXQgYW4gYXBwIHByZXNlbnQgaW4gdGhlIHRvcC1tb3N0IGluc3RhbGxpb24gc2VnbWVudCBtYWtlIHVwIGNsb3NlIDAuNyUgb2YgbWFya2V0LiAgDQoNCkltcGFjdCBvZiBQcmljZQ0KRG9lcyBQcmljZSBoYXZlIGFueXRoaW5nIHRvIGRvIHdpdGggdGhlIG51bWJlciBvZiBpbnN0YWxscz8gIA0KYGBge3IgRURBIC0gIFByaWNlIEltcGFjdCwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpncHMgJT4lIGdncGxvdChhZXMoeCA9IFR5cGUsIHkgPSBhcy5udW1lcmljKGFzLmNoYXJhY3RlcihncHMkSW5zdGFsbHMpKSwgZmlsbCA9IFR5cGUpKStnZW9tX2JveHBsb3QoKStzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygnI0U2OUYwMCcsICcjNTZCNEU5JykpK2xhYnMoeSA9ICJJbnN0YWxscyIsIHggPSAiRnJlZSBvciBQYWlkIiwgdGl0bGUgPSAiSW5mbHVlbmNlIG9mIHR5cGUgb2YgYXZhaWxhYmlsaXR5IG9uIEluc3RhbGxhdGlvbnMiKQ0KDQojVGhpcyBpcyBiZWNhdXNlIG9mIHRoZSBvdXRsaWVycyBwcmVzZW50IGF0IGEgdmVyeSBoaWdoIHZhbHVlLiBUaGlzIGNhbiBiZSBmaXhlZCBieSB1c2luZyB0aGUgbG9nIHRyYW5zZm9ybWF0aW9uLg0KDQpncHMgICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gVHlwZSwgeSA9IGxvZyhhcy5udW1lcmljKGFzLmNoYXJhY3RlcihncHMkSW5zdGFsbHMpKSksIGZpbGwgPSBUeXBlKSkrZ2VvbV9ib3hwbG90KCkrc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoJyNFNjlGMDAnLCAnIzU2QjRFOScpKStsYWJzKHkgPSAiSW5zdGFsbHMgYWZ0ZXIgbG9nIHRyYW5zZm9ybWF0aW9uIiwgeCA9ICJGcmVlIG9yIFBhaWQiLCB0aXRsZSA9ICJJbmZsdWVuY2Ugb2YgdHlwZSBvZiBhdmFpbGFiaWxpdHkgb24gSW5zdGFsbGF0aW9ucyIpDQogICAgICAgICAgIA0KYGBgDQpBcyBleHBlY3RlZCwgZnJlZSBhcHBzIGhhdmUgaGlnaGVyIGluc3RhbGxhdGlvbnMgdGhhbiBwYWlkLiAgDQoNCg0KUHJpY2UgVnMgQ2F0ZWdvcnkNCmBgYHtyIEVEQSAtICBDYXRlZ29yeSBWcyBQcmljZSwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpncHMgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBQcmljZSwgeSA9IENhdGVnb3J5LCBjb2xvciA9IENhdGVnb3J5KSkrDQogIGdlb21faml0dGVyKCkgKyB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpK2xhYnMoeSA9ICJDYXRlZ29yeSIsIHggPSAiUHJpY2UiLCB0aXRsZSA9ICJQcmljZSBWcyBDYXRlZ29yeSIpDQoNCmdwcyAlPiUgc2VsZWN0KGMoIkFwcCIsICJQcmljZSIpKSAlPiUgYXJyYW5nZShkZXNjKFByaWNlKSkgJT4lIGhlYWQoMjApDQpgYGANClRoZSB0b3AgMTUgbW9zdCBleHBlbnNpdmUgYXBwcyBhcHBlYXIgdG8gYmUganVuay4NCg0KSWdub3JpbmcgdGhlbSBhbmQgcHJvY2VlZGluZyB3aXRoIHRoZSBhbmFseXNpcyBnaXZlcw0KYGBge3IgRURBIC0gIENhdGVnb3J5IFZzIFByaWNlIGFmdGVyIHJlbW92aW5nIGp1bmssIG1lc3NhZ2U9Riwgd2FybmluZz1GfSANCmdwcyAlPiUgZmlsdGVyKFByaWNlIDwgODApICU+JQ0KICBnZ3Bsb3QoYWVzKHggPSBQcmljZSwgeSA9IENhdGVnb3J5LCBjb2xvciA9IENhdGVnb3J5KSkrDQogIGdlb21faml0dGVyKCkgKyB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpK2xhYnMoeSA9ICJDYXRlZ29yeSIsIHggPSAiUHJpY2UiLCB0aXRsZSA9ICJQcmljZSBWcyBDYXRlZ29yeSBhZnRlciBjbGVhbmluZyBqdW5rIikNCmBgYA0KVGhlIG1vc3QgZXhwZW5zaXZlIGFwcHMgYmVsb25nIHRvIE1lZGljYWwgYW5kIExpZmVzdHlsZSBmb2xsb3dlZCBieSBGYW1pbHkuICANCg0KVEVYVCBNSU5JTkcgLSBTZW50aW1lbnRhbCBBbmFseXNpcw0KDQpXb3JkIENsb3VkIEZ1bmN0aW9uDQpgYGB7ciBTQSAtIFdvcmQgQ2xvdWQgRnVuY3Rpb24sIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0KDQogIHdvcmRjbG91ZF9jIDwtIGZ1bmN0aW9uKGRhdGEsIG51bV93b3JkcyA9IDEwMCwgYmFja2dyb3VuZCA9ICJ3aGl0ZSIpIHsNCiAgDQogICMgSWYgdGV4dCBpcyBwcm92aWRlZCwgY29udmVydCBpdCB0byBhIGRhdGFmcmFtZSBvZiB3b3JkIGZyZXF1ZW5jaWVzDQogIGlmIChpcy5jaGFyYWN0ZXIoZGF0YSkpIHsNCiAgICBjb3JwdXMgPC0gQ29ycHVzKFZlY3RvclNvdXJjZShkYXRhKSkNCiAgICBjb3JwdXMgPC0gdG1fbWFwKGNvcnB1cywgdG9sb3dlcikNCiAgICBjb3JwdXMgPC0gdG1fbWFwKGNvcnB1cywgcmVtb3ZlUHVuY3R1YXRpb24pDQogICAgY29ycHVzIDwtIHRtX21hcChjb3JwdXMsIHJlbW92ZU51bWJlcnMpDQogICAgY29ycHVzIDwtIHRtX21hcChjb3JwdXMsIHJlbW92ZVdvcmRzLCBzdG9wd29yZHMoImVuZ2xpc2giKSkNCiAgICB0ZG0gPC0gYXMubWF0cml4KFRlcm1Eb2N1bWVudE1hdHJpeChjb3JwdXMpKQ0KICAgIGRhdGEgPC0gc29ydChyb3dTdW1zKHRkbSksIGRlY3JlYXNpbmcgPSBUUlVFKQ0KICAgIGRhdGEgPC0gZGF0YS5mcmFtZSh3b3JkID0gbmFtZXMoZGF0YSksIGZyZXEgPSBhcy5udW1lcmljKGRhdGEpKQ0KICB9DQogIA0KICAjIE1ha2Ugc3VyZSBhIHByb3BlciBudW1fd29yZHMgaXMgcHJvdmlkZWQNCiAgaWYgKCFpcy5udW1lcmljKG51bV93b3JkcykgfHwgbnVtX3dvcmRzIDwgMykgew0KICAgIG51bV93b3JkcyA8LSAzDQogIH0gIA0KICANCiAgIyBHcmFiIHRoZSB0b3AgbiBtb3N0IGNvbW1vbiB3b3Jkcw0KICBkYXRhIDwtIGhlYWQoZGF0YSwgbiA9IG51bV93b3JkcykNCiAgaWYgKG5yb3coZGF0YSkgPT0gMCkgew0KICAgIHJldHVybihOVUxMKQ0KICB9DQogIA0KICB3b3JkY2xvdWQyKGRhdGEsIGJhY2tncm91bmRDb2xvciA9IGJhY2tncm91bmQpDQp9DQoNCmBgYA0KDQpTQSAtIFJldmlldyBBbmFseXNpcw0KDQpUaGUgcmV2aWV3cyBoYWQgc29tZSBzcGVjaWFsIGNoYXJhY3RlcnMsIHdoaWNoIGhhZCB0byBiZSByZW1vdmVkLiAgDQpUaGlzIGRhdGEgaXMgZ3JvdXBlZCB3aXRoIHRoZSBkYXRhIHVzZWQgZm9yIEVEQSBhbmQgYWxsIHRoZSBpcnJlbGV2YW50IGF0dHJpYnV0ZXMgYXJlIGRyb3BwZWQuICAgDQpUaGUgU2VudGltZW50IHBvbGFyaXR5IGFuZCBTZW50aW1lbnQgU3ViamVjdGl2aXR5IHdlcmUgdGhlbiBncm91cGVkIHdpdGggQ2F0ZWdvcnkgdG8gc2VlIGhvdyB0aGV5IGltcGFjdCBpdC4NCg0KYGBge3IgU0EgLSBSQSBwcmUtcHJvY2Vzc2luZywgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpnc3ByIDwtIHJlYWQuY3N2KCJnb29nbGVwbGF5c3RvcmVfdXNlcl9yZXZpZXdzLmNzdiIpICU+JSBuYS5vbWl0KCkNCmdzcHIkQXBwIDwtIGFzLmNoYXJhY3Rlcihnc3ByJEFwcCkNCmdzcHIkVHJhbnNsYXRlZF9SZXZpZXcgPC0gYXMuY2hhcmFjdGVyKGdzcHIkVHJhbnNsYXRlZF9SZXZpZXcpDQpkYXRhIDwtIGdwcyAlPiUgc2VsZWN0KC1jKCJSZXZpZXdzIiwgIlByaWNlIiwgIkxhc3QuVXBkYXRlZCIsICJBbmRyb2lkLlZlciIpKSAlPiUgYXMuZGF0YS5mcmFtZSgpDQpkYXRhJEFwcCA8LSBhcy5jaGFyYWN0ZXIoZGF0YSRBcHApDQpkYXRhIDwtIGRhdGEgJT4lIGlubmVyX2pvaW4oZ3NwciwgYnkgPSAiQXBwIikNCiNnc3ByJExhbmd1YWdlcyA8LSB0ZXh0Y2F0KGdzcHIkVHJhbnNsYXRlZF9SZXZpZXcpDQojZ3NwciA8LSBnc3ByICU+JSBmaWx0ZXIoTGFuZ3VhZ2VzID09ICJlbmdsaXNoIikNCiN3b3JkY2xvdWRfYyhnc3ByWywyXSwgNSkNCiNnZXRfc2VudGltZW50cyhnc3ByWywyXSkNCiNBbGwgdGhlIHNlbnRpbWVudCBleHRyYWN0aW9ucyBoYXMgYWxyZWFkeSBiZWVuIGRvbmUgaW4gdGhlIGRhdGEgc28gcHJvY2VkaW5nIHRvIGNydW5jaCBpbiB0aGF0IGluZm8NCg0KZGF0YSRTZW50aW1lbnRfUG9sYXJpdHkgPC0gcm91bmQoZGF0YSRTZW50aW1lbnRfUG9sYXJpdHksIDIpDQpkYXRhJFNlbnRpbWVudF9TdWJqZWN0aXZpdHkgPC0gcm91bmQoZGF0YSRTZW50aW1lbnRfU3ViamVjdGl2aXR5LCAyKQ0KYGBgDQoNCldvcmQgQ2xvdWRzIGZvciBQb3NpdGl2ZSwgTmV1dHJhbCBhbmQgTmVnYXRpdmUgUmV2aWV3cw0KYGBge3IgU0EgLSBXb3JkIENsb3VkcywgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpkYXRhX3BvIDwtIGRhdGEgJT4lIGZpbHRlcihTZW50aW1lbnRfUG9sYXJpdHkgPT0gMSkgJT4lIHNlbGVjdCgiVHJhbnNsYXRlZF9SZXZpZXciKSANCmRhdGFfbnUgPC0gZGF0YSAlPiUgZmlsdGVyKFNlbnRpbWVudF9Qb2xhcml0eSA9PSAwKSAlPiUgc2VsZWN0KCJUcmFuc2xhdGVkX1JldmlldyIpDQpkYXRhX25lIDwtIGRhdGEgJT4lIGZpbHRlcihTZW50aW1lbnRfUG9sYXJpdHkgPT0gLTEpICU+JSBzZWxlY3QoIlRyYW5zbGF0ZWRfUmV2aWV3IikNCndvcmRjbG91ZF9jKGRhdGFfcG9bLDFdLCAxMDApDQp3b3JkY2xvdWRfYyhkYXRhX251WywxXSwgMTAwKQ0Kd29yZGNsb3VkX2MoZGF0YV9uZVssMV0sIDEwMCkNCg0Kcm0oZGF0YV9uZSkNCnJtKGRhdGFfbnUpDQpybShkYXRhX3BvKQ0KYGBgDQoNClNBIC0gVGV4dCBGb3JlbnNpY3MNCmBgYHtyIFNBIC0gQ2F0ZWdvcnkgVnMgUmV2aWV3cywgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpkYXMgPC0gZGF0YSAlPiUgc2VsZWN0KC0iVHJhbnNsYXRlZF9SZXZpZXciKQ0KZGFzIDwtIGRhcyAlPiUgZ3JvdXBfYnkoQ2F0ZWdvcnksIFNlbnRpbWVudCkgJT4lIHN1bW1hcmlzZShDb3VudCA9IG4oKSwgTlAgPSByb3VuZChzdW0oU2VudGltZW50X1BvbGFyaXR5KS9uKCksMikpDQpkYXMgPC0gZGFzICU+JSBtdXRhdGUocyA9IFNlbnRpbWVudCkNCmRhcyA8LSBkYXMgJT4lIHNwcmVhZChTZW50aW1lbnQsIE5QKQ0KbmFtZXMoZGFzKSA8LSAgYygiQ2F0ZWdvcnkiLCAiQ291bnQiLCAicyIsICJOZV9TIiwgIk51X1MiLCAiUG9fUyIpDQpkYXMgPC0gZGFzICU+JSBzcHJlYWQocywgQ291bnQpDQpuYW1lcyhkYXMpIDwtICBjKCJDYXRlZ29yeSIsICJOZV9TIiwgIk51X1MiLCAiUG9fUyIsICJOZV9DIiwgIk51X0MiLCAiUG9fQyIpDQpkYXNbaXMubmEoZGFzKV0gPC0gMA0KZGFzIDwtIGRhcyAlPiUgZ3JvdXBfYnkoQ2F0ZWdvcnkpICU+JSBzdW1tYXJpc2UoTnVfQyA9IHN1bShOdV9DKSwgTmVfQyA9IHN1bShOZV9DKSwgUG9fQyA9IHN1bShQb19DKSwgTnVfUyA9IHN1bShOdV9TKSwgTmVfUyA9IHN1bShOZV9TKSwgUG9fUyA9IHN1bShQb19TKSkNCg0KDQpkYXMgJT4lIGdncGxvdChhZXMoeSA9IFBvX1MsIHggPSByZW9yZGVyKENhdGVnb3J5LCBQb19TKSwgZmlsbCA9IENhdGVnb3J5KSkrZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpK3RoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikrY29vcmRfZmxpcCgpKw0KICBsYWJzKHkgPSAiU2VudGltZW50IFBvbGFyaXR5IiwgeCA9ICJDYXRlZ29yeSIsIHRpdGxlID0gIlN0cmVuZ3RoIG9mIFBvc2l0aXZlIFNlbnRpbWVudCBWcyBDYXRlZ29yeSIpDQoNCmRhcyAlPiUgZ2dwbG90KGFlcyh5ID0gUG9fQywgeCA9IHJlb3JkZXIoQ2F0ZWdvcnksIFBvX0MpLCBmaWxsID0gQ2F0ZWdvcnkpKStnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5IikrdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKStjb29yZF9mbGlwKCkrDQogIGxhYnMoeSA9ICJOdW1iZXIgb2YgUG9zaXRpdmUgUmV2aWV3cyIsIHggPSAiQ2F0ZWdvcnkiLCB0aXRsZSA9ICJOdW1iZXIgb2YgUG9zaXRpdmUgVnMgQ2F0ZWdvcnkiKQ0KDQpkYXMgJT4lIGdncGxvdChhZXMoeSA9IE5lX1MsIHggPSByZW9yZGVyKENhdGVnb3J5LCAtTmVfUyksIGZpbGwgPSBDYXRlZ29yeSkpK2dlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSt0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpK2Nvb3JkX2ZsaXAoKSsNCiAgbGFicyh5ID0gIlNlbnRpbWVudCBQb2xhcml0eSIsIHggPSAiQ2F0ZWdvcnkiLCB0aXRsZSA9ICJTdHJlbmd0aCBvZiBOZWdhdGl2ZSBTZW50aW1lbnQgVnMgQ2F0ZWdvcnkiKQ0KDQpkYXMgJT4lIGdncGxvdChhZXMoeSA9IE5lX0MsIHggPSByZW9yZGVyKENhdGVnb3J5LCBOZV9DKSwgZmlsbCA9IENhdGVnb3J5KSkrZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpK3RoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikrY29vcmRfZmxpcCgpKw0KICBsYWJzKHkgPSAiTnVtYmVyIG9mIE5lZ2F0aXZlIFJldmlld3MiLCB4ID0gIkNhdGVnb3J5IiwgdGl0bGUgPSAiTnVtYmVyIG9mIE5lZ2F0aXZlIFZzIENhdGVnb3J5IikNCg0KDQpkYXMgJT4lIGdncGxvdChhZXMoeSA9IChQb19DLU5lX0MpLyhOZV9DKSwgeCA9IHJlb3JkZXIoQ2F0ZWdvcnksICgoUG9fQy1OZV9DKS8oTmVfQykpKSwgZmlsbCA9IENhdGVnb3J5KSkrZ2VvbV9iYXIoc3RhdCA9ICJpZGVudGl0eSIpK3RoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJub25lIikrY29vcmRfZmxpcCgpKw0KICBsYWJzKHkgPSAiUGVyY2VudGFnZSBOdW1iZXIgb2YgUG9zaXRpdmUgUmV2aWV3cyB3cnQgTmVnYXRpdmUgUmV2aWV3cyIsIHggPSAiQ2F0ZWdvcnkiLCB0aXRsZSA9ICJQZXJjZW50YWdlIG9mIFBvc2l0aXZlIFJldmlld3MgbW9yZSB0aGFuIE5lZ2F0aXZlIFJldmlld3MiKQ0KDQpkYXMgJT4lIGdncGxvdChhZXMoeSA9IChQb19TLU5lX1MpLCB4ID0gcmVvcmRlcihDYXRlZ29yeSwgKChQb19TLU5lX1MpKSksIGZpbGwgPSBDYXRlZ29yeSkpK2dlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSt0aGVtZShsZWdlbmQucG9zaXRpb24gPSAibm9uZSIpK2Nvb3JkX2ZsaXAoKSsNCiAgbGFicyh5ID0gIlJlbGF0aXZlIHBvbGFyaXR5IG9mIFBvc2l0aXZlIFJldmlld3Mgd3J0IE5lZ2F0aXZlIFJldmlld3MiLCB4ID0gIkNhdGVnb3J5IiwgdGl0bGUgPSAiUmVsYXRpdmUgcG9sYXJpdHkgUG9zaXRpdmUgUmV2aWV3cyB3cnQgTmVnYXRpdmUgUmV2aWV3cyIpDQpybShkYXMpDQpgYGANCg0KRXZlbiB0aG91Z2ggYmVhdXR5IGhhcyBoaWdoIG5lZ2F0aXZlIHJldmlld3MgaXQgaGFzIHRoZSBoaWdoZXN0IHBvbGFyaXR5IGRpZmZlcmVuY2UgYmV0d2VlbiBwb3NpdGl2ZSBhbmQgbmVnYXRpdmUgYW1vbmcgYWxsIHRoZSBjYXRlZ29yaWVzLiBUaGlzIGNhdGVnb3J5IGNhbiBiZSBpZ25vcmVkIGR1ZSB0byBsb3cgbnVtYmVyIG9mIHJldmlld3MuICANCkNvbWljcyBoYXMgdGhlIGhpZ2hlc3QgcmVsYXRpdmUgbnVtYmVyIG9mIHBvc2l0aXZlIHJldmlld3MgY29tcGFyZWQgdG8gdGhhdCBvZiBuZWdhdGl2ZS4gICANCg0KQWxtb3N0IGFsbCB0aGUgY2F0ZWdvcmllcyBoYXZlIHNpbWlsYXIgaW50ZW5zaXRpZXMgd2hlbiBpdCBjb21lcyB0byBwb3NpdGl2ZSBvciBuZWdhdGl2ZSByZWFjdGlvbnMuDQpIZWFsdGggYW5kIGZpdG5lc3MgcmVjZWl2ZWQgdGhlIG1vc3QgcG9zaXRpdmUgcmVhY3Rpb25zIGNvbXBhcmVkIHRvIHRoYXQgb2YgdGhlIG5lZ2F0aXZlIG9uZXMuICANCiAgDQoNCkNvbnRlbnQgVnMgUmV2aWV3cw0KYGBge3IgU0EgQ29udGVudCwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpkYXMgPC0gZGF0YSAlPiUgc2VsZWN0KC1jKCJUcmFuc2xhdGVkX1JldmlldyIsICJBcHAiKSkNCmRhcyAlPiUgZ2dwbG90KGFlcyh4ID0gYXMuZmFjdG9yKENvbnRlbnQuUmF0aW5nKSwgZmlsbCA9IENvbnRlbnQuUmF0aW5nKSkrZ2VvbV9oaXN0b2dyYW0oc3RhdCA9ICJjb3VudCIpK2xhYnMoeSA9ICJOdW1iZXIgb2YgUmV2aWV3cyIsIHggPSAiQ29udGVudCBUeXBlIiwgZmlsbCA9ICJDb250ZW50IFR5cGUiLCB0aXRsZSA9ICJOdW1iZXIgb2YgUmV2aWV3cyBwZXIgY29udGVudCIpK3NjYWxlX2ZpbGxfdmlyaWRpcyhkaXNjcmV0ZSA9IFRSVUUpK2ZhY2V0X3dyYXAoflNlbnRpbWVudCkrY29vcmRfZmxpcCgpDQoNCmRhcyAlPiUgZ2dwbG90KGFlcyh4ID0gYXMuZmFjdG9yKENvbnRlbnQuUmF0aW5nKSwgeSA9IFNlbnRpbWVudF9Qb2xhcml0eSwgY29sb3IgPSBDb250ZW50LlJhdGluZykpK2dlb21fYm94cGxvdCgpK2xhYnMoeSA9ICJQb2xhcml0eSIsIHggPSAiQ29udGVudCBUeXBlIiwgdGl0bGUgPSAiQ29udGVudCBUeXBlIFZzIHRoZSBpbnRlbnNpdHkgb2YgcmV2aWV3cyIpDQoNCmRhcyAlPiUgZ2dwbG90KGFlcyh4ID0gYXMuZmFjdG9yKENvbnRlbnQuUmF0aW5nKSwgZmlsbCA9IFNlbnRpbWVudCkpK2dlb21fYmFyKHN0YXQgPSAiY291bnQiLCBwb3NpdGlvbiA9ICJmaWxsIikrbGFicyh5ID0gIk51bWJlciBvZiBSZXZpZXdzIiwgeCA9ICJDb250ZW50IFR5cGUiLCBmaWxsID0gIkNvbnRlbnQgVHlwZSIsIHRpdGxlID0gIk51bWJlciBvZiBSZXZpZXdzIHBlciBjb250ZW50Iikrc2NhbGVfZmlsbF92aXJpZGlzKGRpc2NyZXRlID0gVFJVRSkNCg0KYGBgDQoNCkFzIGV4cGVjdGVkIHRoZXJlIEV2ZXJ5b25lIGhhcyB0aGUgaGlnaGVzdCBudW1iZXIgb2YgcmV2aWV3cyBpbiBhbGwgY2F0ZWdvcmllcy4gVGhpcyBjb3VsZCBiZSBiZWNhdXNlIHRoZSBpdCBoYXMgdGhlIGhpZ2hlc3QgbnVtYmVyIG9mIGFwcHMgaW4gdGhlIG1hcmtldC4gIA0KQWR1bHQgZm9sbG93ZWQgYnkgTWF0dXJlIEFwcHMgaGF2ZSBoaWdoZXIgbnVtYmVyIG9mIHN0cm9uZyBwb3NpdGl2ZSByZXZpZXdzIHdoaWxlIFRlZW5zIGhhdmUgdGhlIGhpZ2hlciBudW1iZXIgb2Ygc3Ryb25nIG5lZ2F0aXZlIHJldmlld3MuICAgDQoNCg0KDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBQcmUgcHJvY2Vzc2luZw0KDQpIZXJlIGFwYXJ0IGZyb20gZ2VuZXJhbCBwcmVwcm9jZXNzaW5nLCBJIGFtIGRlZmluaW5nIHRoZSBzdWNjZXNzIHRvIGJlIGluc3RhbGxhdGlvbnMgZXF1YWwgdG8gb3IgZ3JlYXRlciB0aGFuIDUwMCwwMDAgaS5lLiB0aGUgYXBwIG11c3QgdGFrZSAwLjAwMDM0JSBvZiB0aGUgdG90YWwgbWFya2V0IGRvd25sb2FkcyB3ZSBjbGFzc2lmeSByZXN1bHQgdG8gYmUgYSDigJxoaXTigJ0sIGFuZCBlbHNlIGl0J3Mg4oCcZmxvcOKAnS4gIA0KVGhlIGNvcnJlbGF0aW9uIGFtb25nIHZhcmlvdXMgdmFyaWFibGUgaXMgYXMgZm9sbG93czoNCg0KYGBge3IgTUwgLSBQcmUgcHJvY2Vzc2luZywgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpybShkYXMpDQpybShkYXRhKQ0KZGF0YSA8LSBncHMgJT4lIHNlbGVjdChjKCJDYXRlZ29yeSIsICJSYXRpbmciLCAiUmV2aWV3cyIsICJTaXplIiwgIlR5cGUiLCAiQ29udGVudC5SYXRpbmciLCAiQ3VycmVudC5WZXIiLCAiSW5zdGFsbHMiKSkNCmdsaW1wc2UoZGF0YSkNCmRhdGEkUmV2aWV3cyA8LSBhcy5pbnRlZ2VyKGFzLmNoYXJhY3RlcihkYXRhJFJldmlld3MpKQ0KZGF0YSRDdXJyZW50LlZlciA8LSBhcy5pbnRlZ2VyKGFzLmNoYXJhY3RlcihkYXRhJEN1cnJlbnQuVmVyKSkNCmRhdGEkSW5zdGFsbHMgPC0gYXMuaW50ZWdlcihhcy5jaGFyYWN0ZXIoZGF0YSRJbnN0YWxscykpDQpkYXRhIDwtIGRhdGEgJT4lIG5hLm9taXQoKQ0KZGF0YSRSYXRpbmcgPC0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoZGF0YSRSYXRpbmcpKQ0KZ2xpbXBzZShkYXRhKQ0KY29yKGRhdGEgJT4lIHNlbGVjdChjKCJSYXRpbmciLCAiUmV2aWV3cyIsICJTaXplIiwgIkN1cnJlbnQuVmVyIiwgIkluc3RhbGxzIikpKQ0KY29ycnBsb3QoY29yKGRhdGEgJT4lIHNlbGVjdChjKCJSYXRpbmciLCAiUmV2aWV3cyIsICJTaXplIiwgIkN1cnJlbnQuVmVyIiwgIkluc3RhbGxzIikpKSwgbWV0aG9kID0gImNvbG9yIiwgdGl0bGUgPSAiQ29ycmVsYXRpb24gUGxvdCIsIG1hcj1jKDAsMCwxLDApKQ0KDQpkYXRhIDwtIGRhdGEgJT4lIG11dGF0ZShSZXN1bHQgPSBhcy5mYWN0b3IoaWZlbHNlKEluc3RhbGxzID49IDUwMDAwMCwgIkhpdCIsICJGbG9wIikpKSAlPiUgc2VsZWN0KC1JbnN0YWxscykNCmBgYA0KSW5zdGFsbHMgaGFzIHRoZSBoaWdoZXN0IGNvcnJlbGF0aW9uIHdpdGggcmV2aWV3cw0KDQpNYWNoaW5lIExlYXJuaW5nIC0gRGF0YSBQYXJ0aXRpb24NCg0KYGBge3IgTUwgLSBEYXRhIFBhcnRpdGlvbiwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpub3JtYWxpemUgPC0gZnVuY3Rpb24oeCl7DQogIHJldHVybiAoKHgtbWluKHgpKS8obWF4KHgpLW1pbih4KSkpDQp9DQpkYXRhLm1sPC0gY2JpbmQoYXMuZGF0YS5mcmFtZShsYXBwbHkoZGF0YVssYygyLDMsNCw3KV0sbm9ybWFsaXplKSksIGRhdGFbLC1jKDIsMyw0LDcpXSkNCg0Kc2V0LnNlZWQoMTIzKQ0KdHJhaW5pbmRleCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKGRhdGEubWwkUmVzdWx0LCBwPTAuOCwgbGlzdD0gRkFMU0UpDQp0cmFpbm4gPC0gZGF0YS5tbFt0cmFpbmluZGV4LCBdDQp0ZXN0biA8LSBkYXRhLm1sWy10cmFpbmluZGV4LCBdDQoNCnNldC5zZWVkKDEyMykNCnRyYWluaW5kZXggPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihkYXRhLm1sJFJlc3VsdCwgcD0wLjgsIGxpc3Q9IEZBTFNFKQ0KdHJhaW5pbmcgPC0gZGF0YS5tbFt0cmFpbmluZGV4LCBdDQp0ZXN0aW5nIDwtIGRhdGEubWxbLXRyYWluaW5kZXgsIF0NCmBgYA0KDQpJIHNwbGl0IHRoZSBkYXRhIGludG8gdHdvIHBhcnRzIHRyYWluaW5nIGFuZCB0ZXN0aW5nIHVzaW5nIGRhdGEgcGFydGl0aW9uIGF0IDgwJSBzcGxpdC4gSSB0aGVuIG5vcm1hbGl6ZWQgdGhlIGNvbnRpbnVvdXMgdmFyaWFibGVzIG9mIHRoZSBkYXRhIGZvciBtb2RlbHMgdGhhdCByZXF1aXJlIG5vcm1hbGl6ZWQgaW5wdXQgYW5kIGFkZGVkIHRoZSBkYXRhIHRoYXQgaXMgbm90IG5vcm1hbGl6ZWQgdG8gbW9kZWxzIHdoaWNoIGRvbuKAmXQgcmVxdWlyZSBub3JtYWxpemVkIGlucHV0cy4NCg0KSSBoYXZlIHVzZWQgdGhlIGZvbGxvd2luZyBtb2RlbHMgb24gdGhlIGRhdGEgdG8gY2xhc3NpZnkgdGhlIHJlY29yZHMgYXMgaGl0IG9yIGZsb3A6DQoNCglMb2dpc3RpYyBSZWdyZXNzaW9uDQoJRGVjaXNpb24gVHJlZXMNCglSYW5kb20gRm9yZXN0DQoJTmHDr3ZlIEJheWVzIENsYXNzaWZpZXINCglTVk0gd2l0aCBEZWZhdWx0IENvc3QgcGFyYW1ldGVyDQoJVHVuZWQgU1ZNIChDb3VsZG7igJl0IGFkZCBpdCBhcyBpdCB0YWtlcyBtb3JlIHRoYW4gMTIgaG91cnMgdG8gcnVuKQ0KDQpUaGVzZSBtb2RlbHMgd2VyZSBydW4gdXBvbiBhbGwgdGhlIGF0dHJpYnV0ZXMgd2l0aCBSZXN1bHQgKOKAnEhpdOKAnSBvciDigJxGbG9w4oCdKSBiZWluZyB0aGUgdGFyZ2V0IGF0dHJpYnV0ZS4NCg0KVGhlIG91dHB1dHMgb2YgdGhlIG1vZGVscyB3ZXJlIGFzIGZvbGxvd3M6DQoNCg0KTWFjaGluZSBMZWFybmluZyAtIE1vZGVsbGluZyAtIExvZ2lzdGljIFJlZ3Jlc3Npb24NCg0KYGBge3IgTUwgLSBMb2dpc3RpYyBSZWdyZXNzaW9uLCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCnJtKHRyYWluaW5kZXgpDQpybShkYXRhLm1sKQ0KdHJhaW4gPC0gdHJhaW5uDQp0ZXN0IDwtIHRlc3RuDQpsb2dpdC5yZWcgPC0gZ2xtKFJlc3VsdCB+LixkYXRhID0gdHJhaW4sIGZhbWlseSA9ICJiaW5vbWlhbCIpDQpzdW1tYXJ5KGxvZ2l0LnJlZykNCg0KDQpsb2dpdC5yZWcgPC0gZ2xtKFJlc3VsdCB+LixkYXRhID0gdHJhaW4gJT4lIHNlbGVjdCgtIkNhdGVnb3J5IiksIGZhbWlseSA9ICJiaW5vbWlhbCIpDQpzdW1tYXJ5KGxvZ2l0LnJlZykNCiNjb25maW50KGxvZ2l0LnJlZykgdG8gY2hlY2sgZm9yIGNvbmZpZGVuY2UgaW50ZXJ2YWxzDQoNCnRlc3QkcHJlZHJhdyA8LSBwcmVkaWN0KGxvZ2l0LnJlZywgbmV3ZGF0YSA9IHRlc3QgJT4lIHNlbGVjdCgtYygiQ2F0ZWdvcnkiLCAiUmVzdWx0IikpLCB0eXBlID0gInJlc3BvbnNlIikNCg0KcGFyIDwtIGRhdGEuZnJhbWUoIkN1dCBvZmYiID0gMSwgIkFjY3VyYWN5IiA9IDEsICJTZW5zaXRpdml0eSIgPSAxKQ0KaSA9IDANCmogPSAxDQpmb3IoaSBpbiBzZXEoMCwxLDAuMDAxKSl7DQogIHRlc3QgPC0gdGVzdCAlPiUgbXV0YXRlKHByZWQgPSBmYWN0b3IoYXMuY2hhcmFjdGVyKGlmZWxzZShwcmVkcmF3IDwgaSwgIkZsb3AiLCAiSGl0IikpLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCiBwYXJbaiwyXSA8LSAxLW1lYW4odGVzdCRSZXN1bHQgIT0gdGVzdCRwcmVkKQ0KIHBhcltqLDFdIDwtIGkNCiB0ZXN0MSA8LSB0ZXN0ICU+JSBmaWx0ZXIoUmVzdWx0ID09ICJIaXQiKQ0KICBwYXJbaiwzXSA8LSBtZWFuKHRlc3QxJFJlc3VsdCA9PSB0ZXN0MSRwcmVkKQ0KaiA8LSBqKzENCmkgPC0gaSsxDQpybSh0ZXN0MSkNCn0NCnJtKGkpDQpybShqKQ0KDQprYWJsZShwYXIgJT4lIGZpbHRlcihBY2N1cmFjeSA9PSBtYXgoQWNjdXJhY3kpKSAlPiUgaGVhZCg1KSwgY2FwdGlvbiA9ICJUaGUgY3V0IG9mZiBmb3IgdGhlIGJlc3QgYWNjdXJhY3kiKQ0KDQpwYXIgJT4lZ2dwbG90KGFlcyh4ID0gQ3V0Lm9mZiwgeSA9IHBhciRBY2N1cmFjeSkpK2dlb21fcG9pbnQoc2l6ZSA9IDAuMDUsIGNvbG9yID0gImdyYXkiKStnZW9tX2xpbmUoKStnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSBtYXgocGFyJEFjY3VyYWN5KSwgY29sb3IgPSAicmVkIikrZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gMC4zNzUsIGNvbG9yID0gInJlZCIpKw0KICBsYWJzKHggPSAiQ3V0IE9mZiB2YWx1ZSIsIHkgPSAiQWNjdXJhY3kgb2YgcHJlZGljdGlvbiIsIHRpdGxlID0gIkN1dCBvZmYgVnMgQWNjdXJhY3kiKQ0Kcm0ocGFyKSAgDQoNCnRlc3QgPC0gdGVzdCAlPiUgbXV0YXRlKHByZWQgPSBhcy5mYWN0b3IoaWZlbHNlKHByZWRyYXcgPD0gMC4zNSwgIkZsb3AiLCAiSGl0IikpKQ0KdGVzdCRwcmVkIDwtIGZhY3Rvcih0ZXN0JHByZWQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQp0ZXN0JFJlc3VsdCA8LSBmYWN0b3IodGVzdCRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQpjbSA8LSBjb25mdXNpb25NYXRyaXgodGVzdCRwcmVkLCB0ZXN0JFJlc3VsdCkNCmNtDQpsb2dpdCA8LSBkYXRhLmZyYW1lKCJNb2RlbCIgPSAiTG9naXN0aWMgUmVncmVzc2lvbiIsICJBY2N1cmFjeSIgPSBjbVtbIm92ZXJhbGwiXV1bWyJBY2N1cmFjeSJdXSwgIlNlbnNpdGl2aXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCAiU3BlY2lmaWNpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU3BlY2lmaWNpdHkiXV0pDQpgYGANCkxvZ2lzdGljIHJlZ3Jlc3Npb24gaGFzIHBlcmZvcm1lZCBwcmV0dHkgd2VsbCBpbiBhbGwgdGhlIGFzcGVjdHMuIE5vdGU6IEl0IG11c3QgcmVtZW1iZXJlZCB0aGF0IG91ciBjbGFzcyBvZiBpbnRlcmVzdCBpcyBIaXQuDQoNCg0KTWFjaGluZSBMZWFybmluZyAtIE1vZGVsbGluZyAtIERlY2lzaW9uIFRyZWVzDQpgYGB7ciBNTCAtIERlY2lzaW9uIFRyZWVzLCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCnJtKHRlc3QpDQpybSh0cmFpbikNCnJtKGxvZ2l0LnJlZykNCnRyYWluIDwtIHRyYWluaW5nDQp0ZXN0IDwtIHRlc3RpbmcNCg0Kc2V0LnNlZWQoMTIzKQ0KZHQgPC0gcnBhcnQoUmVzdWx0IH4uLCBkYXRhID0gdHJhaW4sIG1ldGhvZCA9ICJjbGFzcyIsIGNwID0gMC4wMDAwMSwgbWluc3BsaXQgPSAxLCB4dmFsID0gMTApDQpkdC5wcnVuZWQgPC0gcHJ1bmUoZHQsIGNwID0gMC4wMDQpDQoNCnRlc3QkcHJlZCA8LSBwcmVkaWN0KGR0LnBydW5lZCwgbmV3ZGF0YSA9IHRlc3RbLC04XSwgdHlwZSA9ICJjbGFzcyIpDQoNCmNtIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodGVzdCRwcmVkLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSwgZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCmNtDQpkZXQgPC0gZGF0YS5mcmFtZSgiTW9kZWwiID0gIkRlY2lzaW9uIFRyZWVzIiwgIkFjY3VyYWN5IiA9IGNtW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCAiU2Vuc2l0aXZpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sICJTcGVjaWZpY2l0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTcGVjaWZpY2l0eSJdXSkNCnJtKGNtKQ0KcnBhcnQucGxvdChkdC5wcnVuZWQsIGJveC5wYWxldHRlPSJSZEJ1Iiwgc2hhZG93LmNvbD0iZ3JheSIsIG5uPVRSVUUpDQoNCnJwYXJ0LnJ1bGVzKGR0LnBydW5lZCwgc3R5bGUgPSAidGFsbHciKQ0KYGBgDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBNb2RlbGxpbmcgLSBSYW5kb20gRm9yZXN0DQpgYGB7ciBNTCAtIFJhbmRvbSBGb3Jlc3QsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0Kcm0odHJhaW4pDQpybSh0ZXN0KQ0Kcm0oZHQpDQpybShkdC5wcnVuZWQpDQp0cmFpbiA8LSB0cmFpbmluZw0KdGVzdCA8LSB0ZXN0aW5nDQpyZiA8LSByYW5kb21Gb3Jlc3QoUmVzdWx0IH4uLCBkYXRhID0gdHJhaW4pDQpwbG90KHJmLCBtYWluID0gIkVycm9yIHJhdGUgb2YgcmFuZG9tIGZvcmVzdCIpDQp2YXJJbXBQbG90KHJmLCBtYWluID0gIkdpbmkgUGxvdCAtIEltcG9ydGFuY2Ugb2YgVmFyaWFibGVzIikNCnRlc3QkcHJlZCA9IHByZWRpY3QocmYsIG5ld2RhdGEgPSB0ZXN0Wy04XSkNCmNtIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodGVzdCRwcmVkLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSwgZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCmNtDQpyYWYgPC0gZGF0YS5mcmFtZSgiTW9kZWwiID0gIlJhbmRvbSBGb3Jlc3QiLCAiQWNjdXJhY3kiID0gY21bWyJvdmVyYWxsIl1dW1siQWNjdXJhY3kiXV0sICJTZW5zaXRpdml0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgIlNwZWNpZmljaXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNwZWNpZmljaXR5Il1dKQ0Kcm0oY20pDQpgYGANCg0KTWFjaGluZSBMZWFybmluZyAtIE1vZGVsbGluZyAtIE5haXZlIEJheWVzIENsYXNzaWZpZXINCmBgYHtyIE1MIC0gTmFpdmUgQmF5ZXMgQ2xhc3NpZmllciwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpybSh0ZXN0KQ0Kcm0odHJhaW4pDQpybShyZikNCnRyYWluIDwtIHRyYWluaW5nDQp0cmFpbiRSZXN1bHQgPC0gZmFjdG9yKHRyYWluJFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkNCnRlc3QgPC0gdGVzdGluZw0KdGVzdCRSZXN1bHQgPC0gZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKQ0KDQpkZWxheXMubmIgPC0gbmFpdmVCYXllcyhSZXN1bHR+LiwgZGF0YSA9IHRyYWluKQ0KZGVsYXlzLm5iDQoNCnByZWQucHJvYiA8LSBhcy5kYXRhLmZyYW1lKHByZWRpY3QoZGVsYXlzLm5iLCBuZXdkYXRhID0gdGVzdCwgdHlwZSA9ICJyYXciKSkNCnByZWQucHJvYiA8LSAgcm91bmQocHJlZC5wcm9iLCA0KQ0KcHJlZC5wcm9iIDwtIHByZWQucHJvYiAlPiUgc2VsZWN0KCJIaXQiKQ0KbmFtZXMocHJlZC5wcm9iKSA8LSAgInByZWRyYXciDQp0ZXN0IDwtIGNiaW5kKHRlc3QsIHByZWQucHJvYikNCnJtKHByZWQucHJvYikNCg0KcGFyIDwtIGRhdGEuZnJhbWUoIkN1dCBvZmYiID0gMSwgIkFjY3VyYWN5IiA9IDEsICJTZW5zaXRpdml0eSIgPSAxKQ0KaSA9IDANCmogPSAxDQpmb3IoaSBpbiBzZXEoMCwxLDAuMDAxKSl7DQogIHRlc3QgPC0gdGVzdCAlPiUgbXV0YXRlKHByZWQgPSBmYWN0b3IoYXMuY2hhcmFjdGVyKGlmZWxzZShwcmVkcmF3IDwgaSwgIkZsb3AiLCAiSGl0IikpLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCiBwYXJbaiwyXSA8LSAxLW1lYW4odGVzdCRSZXN1bHQgIT0gdGVzdCRwcmVkKQ0KIHRlc3QxIDwtIHRlc3QgJT4lIGZpbHRlcihSZXN1bHQgPT0gIkhpdCIpDQogcGFyW2osM10gPC0gbWVhbih0ZXN0MSRSZXN1bHQgPT0gdGVzdDEkcHJlZCkNCiBwYXJbaiwxXSA8LSBpDQpqIDwtIGorMQ0KaSA8LSBpKzENCnJtKHRlc3QxKQ0KfQ0Kcm0oaSkNCnJtKGopDQoNCmthYmxlKHBhciAlPiUgZmlsdGVyKEFjY3VyYWN5ID09IG1heChBY2N1cmFjeSkpICU+JSBoZWFkKDUpLCBjYXB0aW9uID0gIlRoZSAoSGl0KSBwcm9iYWJpbGl0eSBjdXQgb2ZmIGZvciB0aGUgYmVzdCBhY2N1cmFjeSIpDQoNCnBhciAlPiUgZ2dwbG90KGFlcyh4ID0gYEN1dC5vZmZgLCB5ID0gcGFyJEFjY3VyYWN5KSkrZ2VvbV9wb2ludChzaXplID0gMC4wNSwgY29sb3IgPSAiZ3JheSIpK2dlb21fbGluZSgpK2dlb21faGxpbmUoeWludGVyY2VwdCA9IG1heChwYXIkQWNjdXJhY3kpLCBjb2xvciA9ICJyZWQiKStnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAwLjAyNCwgY29sb3IgPSAicmVkIikrDQogIGxhYnMoeCA9ICJDdXQgT2ZmIHZhbHVlIiwgeSA9ICJBY2N1cmFjeSBvZiBwcmVkaWN0aW9uIiwgdGl0bGUgPSAiQ3V0IG9mZiBWcyBBY2N1cmFjeSIpDQoNCnJtKHBhcikNCg0KdGVzdCA8LSB0ZXN0ICU+JSBtdXRhdGUocHJlZCA9ICBmYWN0b3IoYXMuY2hhcmFjdGVyKGlmZWxzZShwcmVkcmF3IDwgMC4wMjQsICJGbG9wIiwgIkhpdCIpKSwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkpDQoNCmNtIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodGVzdCRwcmVkLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSwgZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCmNtDQpuYWIgPC0gZGF0YS5mcmFtZSgiTW9kZWwiID0gIk5haXZlIEJheWVzIiwgIkFjY3VyYWN5IiA9IGNtW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCAiU2Vuc2l0aXZpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sICJTcGVjaWZpY2l0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTcGVjaWZpY2l0eSJdXSkNCnJtKGNtKQ0KYGBgDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBNb2RlbGxpbmcgLSBTVk0NCmBgYHtyIE1MIC0gU1ZNLCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCnJtKHRlc3QpDQpybSh0cmFpbikNCnJtKGRlbGF5cy5uYikNCg0KdHJhaW4gPC0gdHJhaW5pbmcNCnRyYWluJFJlc3VsdCA8LSBmYWN0b3IodHJhaW4kUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKQ0KdGVzdCA8LSB0ZXN0aW5nDQp0ZXN0JFJlc3VsdCA8LSBmYWN0b3IodGVzdCRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQoNCnNldC5zZWVkKDEyMykNCnN2bS5maXQgPC0gc3ZtKFJlc3VsdH4uLCBkYXRhPXRyYWluLCBrZXJuZWwgPSAibGluZWFyIikNCg0KdGVzdCRwcmVkIDwtIHByZWRpY3Qoc3ZtLmZpdCwgbmV3ZGF0YSA9IHRlc3RbLC04XSwgdHlwZSA9ICJjbGFzcyIpDQpjbSA8LSBjb25mdXNpb25NYXRyaXgoZmFjdG9yKHRlc3QkcHJlZCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSksIGZhY3Rvcih0ZXN0JFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkpDQpjbQ0KDQpsaW5lYXIgPC0gZGF0YS5mcmFtZSgiTU9kZWwiID0gIlNWTSAtIExpbmVhciIsICJBY2N1cmFjeSIgPSBjbVtbIm92ZXJhbGwiXV1bWyJBY2N1cmFjeSJdXSwgIlNlbnNpdGl2aXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCAiU3BlY2lmaWNpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU3BlY2lmaWNpdHkiXV0pDQoNCnJtKHRlc3QpDQpybSh0cmFpbikNCnJtKHN2bS5maXQpDQpybShjbSkNCg0KdHJhaW4gPC0gdHJhaW5pbmcNCnRyYWluJFJlc3VsdCA8LSBmYWN0b3IodHJhaW4kUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKQ0KdGVzdCA8LSB0ZXN0aW5nDQp0ZXN0JFJlc3VsdCA8LSBmYWN0b3IodGVzdCRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQoNCnNldC5zZWVkKDEyMykNCnN2bS5maXQgPC0gc3ZtKFJlc3VsdH4uLCBkYXRhPXRyYWluLCBrZXJuZWwgPSAicmFkaWFsIikNCg0KdGVzdCRwcmVkIDwtIHByZWRpY3Qoc3ZtLmZpdCwgbmV3ZGF0YSA9IHRlc3RbLC04XSwgdHlwZSA9ICJjbGFzcyIpDQpjbSA8LSBjb25mdXNpb25NYXRyaXgoZmFjdG9yKHRlc3QkcHJlZCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSksIGZhY3Rvcih0ZXN0JFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkpDQpjbQ0KDQpyYWRpYWwgPC0gZGF0YS5mcmFtZSgiTU9kZWwiID0gIlNWTSAtIFJhZGlhbCIsICJBY2N1cmFjeSIgPSBjbVtbIm92ZXJhbGwiXV1bWyJBY2N1cmFjeSJdXSwgIlNlbnNpdGl2aXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCAiU3BlY2lmaWNpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU3BlY2lmaWNpdHkiXV0pDQoNCnJtKHRlc3QpDQpybSh0cmFpbikNCnJtKHN2bS5maXQpDQpybShjbSkNCg0KdHJhaW4gPC0gdHJhaW5pbmcNCnRyYWluJFJlc3VsdCA8LSBmYWN0b3IodHJhaW4kUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKQ0KdGVzdCA8LSB0ZXN0aW5nDQp0ZXN0JFJlc3VsdCA8LSBmYWN0b3IodGVzdCRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQoNCnNldC5zZWVkKDEyMykNCnN2bS5maXQgPC0gc3ZtKFJlc3VsdH4uLCBkYXRhPXRyYWluLCBrZXJuZWwgPSAicG9seW5vbWlhbCIpDQoNCnRlc3QkcHJlZCA8LSBwcmVkaWN0KHN2bS5maXQsIG5ld2RhdGEgPSB0ZXN0WywtOF0sIHR5cGUgPSAiY2xhc3MiKQ0KY20gPC0gY29uZnVzaW9uTWF0cml4KGZhY3Rvcih0ZXN0JHByZWQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpLCBmYWN0b3IodGVzdCRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpKQ0KY20NCg0KcG9seW5vbWlhbCA8LSBkYXRhLmZyYW1lKCJNT2RlbCIgPSAiU1ZNIC0gUG9seW5vbWlhbCIsICJBY2N1cmFjeSIgPSBjbVtbIm92ZXJhbGwiXV1bWyJBY2N1cmFjeSJdXSwgIlNlbnNpdGl2aXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCAiU3BlY2lmaWNpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU3BlY2lmaWNpdHkiXV0pDQoNCnJtKGNtKQ0Kcm0odGVzdCkNCnJtKHRyYWluKQ0Kcm0oc3ZtLmZpdCkNCg0Kc3ZtIDwtIHJiaW5kKGxpbmVhciwgcmFkaWFsLCBwb2x5bm9taWFsKQ0Kcm0obGluZWFyKQ0Kcm0ocmFkaWFsKQ0Kcm0ocG9seW5vbWlhbCkNCm5hbWVzKHN2bSkgPC0gYygiTW9kZWwiLCAiQWNjdXJhY3kiLCAiU2Vuc2l0aXZpdHkiLCAiU3BlY2lmaWNpdHkiKQ0KDQpgYGANCg0KDQpNYWNoaW5lIExlYXJuaW5nIC0gTW9kZWxsaW5nIC0gU1ZNIChUdW5pbmcpDQoNCmBgYHtyIE1MIC0gU1ZNIFR1bmluZywgZXZhbCA9IEZBTFNFLCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCnNldC5zZWVkKDEyMykNCnR1bmVzdm0gPC0gdHVuZShzdm0sIFJlc3VsdH4uLCBkYXRhID0gdHJhaW4sIGtlcm5hbCA9ICJsaW5lYXIiLCByYW5nZXMgPSBsaXN0KGNvc3QgPSBjKHNlcSgwLjAxLDAuMSxieSA9IDAuMDEpLCBzZXEoMC4xLDEsYnkgPSAwLjEpLCBzZXEoMSwxMCxieSA9IDEpKSkpDQpkeW5hbWljLmNvc3QgPC0gdHVuZXN2bSRiZXN0Lm1vZGVsJGNvc3QNCnN1bW1hcnkodHVuZXN2bSkNCmRhdGEucGFyIDwtIHR1bmVzdm0kcGVyZm9ybWFuY2VzDQpkYXRhLnBhcltkYXRhLnBhciRlcnJvciA9PSBtaW4oZGF0YS5wYXIkZXJyb3IpLF0NCg0Kc2V0LnNlZWQoMTIzKQ0Kc3ZtX2Nvc3QgPC0gc3ZtKFJlc3VsdH4uLCBkYXRhPXRyYWluLGtlcm5lbCA9ICJsaW5lYXIiLCBjb3N0ID0gZHluYW1pYy5jb3N0KQ0KcHJlZF90cmFpbiA8LSBwcmVkaWN0KHN2bV9jb3N0LCB0cmFpbikNCnRyYWluLmVycm9yIDwtIG1lYW4ocHJlZF90cmFpbiAhPSB0cmFpbiRSZXN1bHQpDQpwcmVkX3Rlc3QgPC0gcHJlZGljdChzdm1fY29zdCwgdGVzdCkNCnRlc3QuZXJyb3IgPC0gbWVhbihwcmVkX3Rlc3QgIT0gdGVzdCRSZXN1bHQpDQpsaW5lYXIgPC0gZGF0YS5mcmFtZSgiS2VybmFsIiA9ICJMaW5lYXIiLCAiQ29zdCIgPSBkeW5hbWljLmNvc3QsDQogICAgICAgICAgICAgICAgICAgICAidHJhaW4gRXJyb3IiID0gdHJhaW4uZXJyb3IsIA0KICAgICAgICAgICAgICAgICAgICAgInRlc3QgRXJyb3IiID0gdGVzdC5lcnJvcikNCg0Kc2V0LnNlZWQoMTIzKQ0KdHVuZXN2bSA8LSB0dW5lKHN2bSwgUmVzdWx0fi4sIGRhdGEgPSB0cmFpbiwga2VybmFsID0gInJhZGlhbCIsIHJhbmdlcyA9IGxpc3QoY29zdCA9IGMoc2VxKDAuMDEsMC4xLGJ5ID0gMC4wMSksIHNlcSgwLjEsMSxieSA9IDAuMSksIHNlcSgxLDEwLGJ5ID0gMSkpKSkNCmR5bmFtaWMuY29zdCA8LSB0dW5lc3ZtJGJlc3QubW9kZWwkY29zdA0Kc3VtbWFyeSh0dW5lc3ZtKQ0KZGF0YS5wYXIgPC0gdHVuZXN2bSRwZXJmb3JtYW5jZXMNCmRhdGEucGFyW2RhdGEucGFyJGVycm9yID09IG1pbihkYXRhLnBhciRlcnJvciksXSANCg0KDQpzdm1fY29zdCA8LSBzdm0oUmVzdWx0fi4sIGRhdGE9dHJhaW4sIGtlcm5lbCA9ICJyYWRpYWwiLCBjb3N0ID0gZHluYW1pYy5jb3N0KQ0KcHJlZF90cmFpbiA8LSBwcmVkaWN0KHN2bV9jb3N0LCB0cmFpbikNCnRyYWluLmVycm9yIDwtIG1lYW4ocHJlZF90cmFpbiAhPSB0cmFpbiRSZXN1bHQpDQpwcmVkX3Rlc3QgPC0gcHJlZGljdChzdm1fY29zdCwgdGVzdCkNCnRlc3QuZXJyb3IgPC0gbWVhbihwcmVkX3Rlc3QgIT0gdGVzdCRSZXN1bHQpDQpyYWRpYWwgPC0gZGF0YS5mcmFtZSgiS2VybmFsIiA9ICJSYWRpYWwiLCAiQ29zdCIgPSBkeW5hbWljLmNvc3QsDQogICAgICAgICAgICAgICAgICAgICAidHJhaW4gRXJyb3IiID0gIHRyYWluLmVycm9yLCANCiAgICAgICAgICAgICAgICAgICAgICJ0ZXN0IEVycm9yIiA9IHRlc3QuZXJyb3IpDQoNCnNldC5zZWVkKDEyMykNCnR1bmVzdm0gPC0gdHVuZShzdm0sIFJlc3VsdH4uLCBkYXRhID0gdHJhaW4sIGtlcm5hbCA9ICJwb2x5bm9taWFsIiwgZGVncmVlID0gMiwgcmFuZ2VzID0gbGlzdChjb3N0ID0gYyhzZXEoMC4wMSwwLjEsIGJ5ID0gMC4wMSksIHNlcSgwLjEsMSxieSA9IDAuMSksIHNlcSgxLDEwLGJ5ID0gMSkpKSkNCmR5bmFtaWMuY29zdCA8LSB0dW5lc3ZtJGJlc3QubW9kZWwkY29zdA0Kc3VtbWFyeSh0dW5lc3ZtKQ0KZGF0YS5wYXIgPC0gdHVuZXN2bSRwZXJmb3JtYW5jZXMNCmRhdGEucGFyW2RhdGEucGFyJGVycm9yID09IG1pbihkYXRhLnBhciRlcnJvciksXQ0Kc3ZtX2Nvc3QgPC0gc3ZtKFJlc3VsdH4uLCBkYXRhPXRyYWluLCBrZXJuZWwgPSAicG9seW5vbWlhbCIsIGRlZ3JlZSA9IDIsIGNvc3QgPSBkeW5hbWljLmNvc3QpDQpwcmVkX3RyYWluIDwtIHByZWRpY3Qoc3ZtX2Nvc3QsIHRyYWluKQ0KdHJhaW4uZXJyb3IgPC0gbWVhbihwcmVkX3RyYWluICE9IHRyYWluJFJlc3VsdCkNCnByZWRfdGVzdCA8LSBwcmVkaWN0KHN2bV9jb3N0LCB0ZXN0KQ0KdGVzdC5lcnJvciA8LSBtZWFuKHByZWRfdGVzdCAhPSB0ZXN0JFJlc3VsdCkNCnBvbHlub21pYWwgPC0gZGF0YS5mcmFtZSgiS2VybmFsIiA9ICJQb2x5bm9taWFsIiwgIkNvc3QiID0gZHluYW1pYy5jb3N0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICJ0cmFpbiBFcnJvciIgPSAgdHJhaW4uZXJyb3IsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICJ0ZXN0IEVycm9yIiA9IHRlc3QuZXJyb3IpDQoNCg0Ka2FibGUoZmluYWwgPC0gcmJpbmQobGluZWFyLCByYWRpYWwsIHBvbHlub21pYWwpLCBjYXB0aW9uID0gIlNWTSBQZXJmb3JtYW5jZSB3aXRoIGRpZmZlcmVudCBrZXJuYWxzIikNCmBgYA0KDQoNCg0KSW4gdGhpcyBhbmFseXNpcyBJIHVzZWQgUmF0aW5nIGFuZCBSZXZpZXdzIHdoaWNoIGFyZSBrbm93biBvbmx5IGFmdGVyIGFuIEFwcCBpcyBsYXVuY2hlZC4gU28gdGhlIGFuYWx5c2lzIEkgZGlkIHNvIGZhciBpcyBwdXJlbHkgaHlwb3RoZXRpY2FsLg0KRm9yIHRoaXMgcmVhc29uIEkgYW0gcmVwZWF0aW5nIHRoZSBlbnRpcmUgYW5hbHlzaXMgZHJvcHBpbmcgUmF0aW5nIGFuZCBSZXZpZXdzIEF0dHJpYnV0ZXMuDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBNb2RlbGxpbmcgLSBMb2dpc3RpYyBSZWdyZXNzaW9uDQoNCmBgYHtyIE1MIC0gTG9naXN0aWMgUmVncmVzc2lvbiBmb3IgUmVhbCBEYXRhLCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCnJtKHRyYWluaW5kZXgpDQpybShkYXRhLm1sKQ0KdHJhaW4gPC0gdHJhaW5uICU+JSBzZWxlY3QoLWMoIlJhdGluZyIsICJSZXZpZXdzIikpDQp0ZXN0IDwtIHRlc3RuICU+JSBzZWxlY3QoLWMoIlJhdGluZyIsICJSZXZpZXdzIikpDQoNCmxvZ2l0LnJlZyA8LSBnbG0oUmVzdWx0IH4uLGRhdGEgPSB0cmFpbiwgZmFtaWx5ID0gImJpbm9taWFsIikNCnN1bW1hcnkobG9naXQucmVnKQ0KI2NvbmZpbnQobG9naXQucmVnKSB0byBjaGVjayBmb3IgY29uZmlkZW5jZSBpbnRlcnZhbHMNCg0KdGVzdCRwcmVkcmF3IDwtIHByZWRpY3QobG9naXQucmVnLCBuZXdkYXRhID0gdGVzdCAlPiUgc2VsZWN0KC0iUmVzdWx0IiksIHR5cGUgPSAicmVzcG9uc2UiKQ0KDQpwYXIgPC0gZGF0YS5mcmFtZSgiQ3V0IG9mZiIgPSAxLCAiQWNjdXJhY3kiID0gMSwgIlNlbnNpdGl2aXR5IiA9IDEpDQppID0gMA0KaiA9IDENCmZvcihpIGluIHNlcSgwLDEsMC4wMDEpKXsNCiAgdGVzdCA8LSB0ZXN0ICU+JSBtdXRhdGUocHJlZCA9IGZhY3Rvcihhcy5jaGFyYWN0ZXIoaWZlbHNlKHByZWRyYXcgPCBpLCAiRmxvcCIsICJIaXQiKSksIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpKQ0KIHBhcltqLDJdIDwtIDEtbWVhbih0ZXN0JFJlc3VsdCAhPSB0ZXN0JHByZWQpDQogcGFyW2osMV0gPC0gaQ0KIHRlc3QxIDwtIHRlc3QgJT4lIGZpbHRlcihSZXN1bHQgPT0gIkhpdCIpDQogIHBhcltqLDNdIDwtIG1lYW4odGVzdDEkUmVzdWx0ID09IHRlc3QxJHByZWQpDQpqIDwtIGorMQ0KaSA8LSBpKzENCnJtKHRlc3QxKQ0KfQ0Kcm0oaSkNCnJtKGopDQoNCmthYmxlKHBhciAlPiUgZmlsdGVyKEFjY3VyYWN5ID09IG1heChBY2N1cmFjeSkpICU+JSBoZWFkKDUpLCBjYXB0aW9uID0gIlRoZSBjdXQgb2ZmIGZvciB0aGUgYmVzdCBhY2N1cmFjeSIpDQoNCnBhciAlPiVnZ3Bsb3QoYWVzKHggPSBDdXQub2ZmLCB5ID0gcGFyJEFjY3VyYWN5KSkrZ2VvbV9wb2ludChzaXplID0gMC4wNSwgY29sb3IgPSAiZ3JheSIpK2dlb21fbGluZSgpK2dlb21faGxpbmUoeWludGVyY2VwdCA9IG1heChwYXIkQWNjdXJhY3kpLCBjb2xvciA9ICJyZWQiKStnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAwLjQ0MywgY29sb3IgPSAicmVkIikrDQogIGxhYnMoeCA9ICJDdXQgT2ZmIHZhbHVlIiwgeSA9ICJBY2N1cmFjeSBvZiBwcmVkaWN0aW9uIiwgdGl0bGUgPSAiQ3V0IG9mZiBWcyBBY2N1cmFjeSIpDQpybShwYXIpICANCg0KdGVzdCA8LSB0ZXN0ICU+JSBtdXRhdGUocHJlZCA9IGFzLmZhY3RvcihpZmVsc2UocHJlZHJhdyA8PSAwLjQ0MywgIkZsb3AiLCAiSGl0IikpKQ0KdGVzdCRwcmVkIDwtIGZhY3Rvcih0ZXN0JHByZWQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQp0ZXN0JFJlc3VsdCA8LSBmYWN0b3IodGVzdCRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQpjbSA8LSBjb25mdXNpb25NYXRyaXgodGVzdCRwcmVkLCB0ZXN0JFJlc3VsdCkNCmNtDQpsb2dpdC51IDwtIGRhdGEuZnJhbWUoIk1vZGVsIiA9ICJMb2dpc3RpYyBSZWdyZXNzaW9uIChVKSIsICJBY2N1cmFjeSIgPSBjbVtbIm92ZXJhbGwiXV1bWyJBY2N1cmFjeSJdXSwgIlNlbnNpdGl2aXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCAiU3BlY2lmaWNpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU3BlY2lmaWNpdHkiXV0pDQpybShjbSkNCmBgYA0KDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBNb2RlbGxpbmcgLSBEZWNpc2lvbiBUcmVlcw0KYGBge3IgTUwgLSBEZWNpc2lvbiBUcmVlcyBmb3IgUmVhbCBEYXRhLCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCnJtKHRlc3QpDQpybSh0cmFpbikNCnJtKGxvZ2l0LnJlZykNCnRyYWluIDwtIHRyYWluaW5nICU+JSBzZWxlY3QoLWMoIlJhdGluZyIsICJSZXZpZXdzIikpDQp0ZXN0IDwtIHRlc3RpbmcgJT4lIHNlbGVjdCgtYygiUmF0aW5nIiwgIlJldmlld3MiKSkNCg0Kc2V0LnNlZWQoMTIzKQ0KZHQgPC0gcnBhcnQoUmVzdWx0IH4uLCBkYXRhID0gdHJhaW4sIG1ldGhvZCA9ICJjbGFzcyIsIGNwID0gMC4wMDAwMSwgbWluc3BsaXQgPSAxLCB4dmFsID0gMTApDQpkdC5wcnVuZWQgPC0gcHJ1bmUoZHQsIGNwID0gMC4wMDQpDQoNCnRlc3QkcHJlZCA8LSBwcmVkaWN0KGR0LnBydW5lZCwgbmV3ZGF0YSA9IHRlc3RbLC02XSwgdHlwZSA9ICJjbGFzcyIpDQoNCmNtIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodGVzdCRwcmVkLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSwgZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCmNtDQpkZXQudSA8LSBkYXRhLmZyYW1lKCJNb2RlbCIgPSAiRGVjaXNpb24gVHJlc3MgKFUpIiwgIkFjY3VyYWN5IiA9IGNtW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCAiU2Vuc2l0aXZpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sICJTcGVjaWZpY2l0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTcGVjaWZpY2l0eSJdXSkNCnJtKGNtKQ0KcnBhcnQucGxvdChkdC5wcnVuZWQsIGJveC5wYWxldHRlPSJSZEJ1Iiwgc2hhZG93LmNvbD0iZ3JheSIsIG5uPVRSVUUpDQoNCnJwYXJ0LnJ1bGVzKGR0LnBydW5lZCwgc3R5bGUgPSAidGFsbHciKQ0KYGBgDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBNb2RlbGxpbmcgLSBSYW5kb20gRm9yZXN0DQpgYGB7ciBNTCAtIFJhbmRvbSBGb3Jlc3QgIGZvciBSZWFsIERhdGEsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0Kcm0odHJhaW4pDQpybSh0ZXN0KQ0Kcm0oZHQpDQpybShkdC5wcnVuZWQpDQp0cmFpbiA8LSB0cmFpbmluZyAlPiUgc2VsZWN0KC1jKCJSYXRpbmciLCAiUmV2aWV3cyIpKQ0KdGVzdCA8LSB0ZXN0aW5nICU+JSBzZWxlY3QoLWMoIlJhdGluZyIsICJSZXZpZXdzIikpDQpyZiA8LSByYW5kb21Gb3Jlc3QoUmVzdWx0IH4uLCBkYXRhID0gdHJhaW4pDQojcGxvdChyZiwgbWFpbiA9ICJFcnJvciByYXRlIG9mIHJhbmRvbSBmb3Jlc3QiKQ0KdmFySW1wUGxvdChyZiwgbWFpbiA9ICJHaW5pIFBsb3QgLSBJbXBvcnRhbmNlIG9mIFZhcmlhYmxlcyIpDQp0ZXN0JHByZWQgPSBwcmVkaWN0KHJmLCBuZXdkYXRhID0gdGVzdFstNl0pDQpjbSA8LSBjb25mdXNpb25NYXRyaXgoZmFjdG9yKHRlc3QkcHJlZCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSksIGZhY3Rvcih0ZXN0JFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkpDQpjbQ0KcmFmLnUgPC0gZGF0YS5mcmFtZSgiTW9kZWwiID0gIlJhbmRvbSBGb3Jlc3QgKFUpIiwgIkFjY3VyYWN5IiA9IGNtW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCAiU2Vuc2l0aXZpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sICJTcGVjaWZpY2l0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTcGVjaWZpY2l0eSJdXSkNCnJtKGNtKQ0KYGBgDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBNb2RlbGxpbmcgLSBOYWl2ZSBCYXllcyBDbGFzc2lmaWVyDQpgYGB7ciBNTCAtIE5haXZlIEJheWVzIENsYXNzaWZpZXIgZm9yIFJlYWwgRGF0YSwgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQpybSh0ZXN0KQ0Kcm0odHJhaW4pDQpybShyZikNCnRyYWluIDwtIHRyYWluaW5nICU+JSBzZWxlY3QoLWMoIlJhdGluZyIsICJSZXZpZXdzIikpDQp0cmFpbiRSZXN1bHQgPC0gZmFjdG9yKHRyYWluJFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkNCnRlc3QgPC0gdGVzdGluZyAlPiUgc2VsZWN0KC1jKCJSYXRpbmciLCAiUmV2aWV3cyIpKQ0KdGVzdCRSZXN1bHQgPC0gZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKQ0KDQpkZWxheXMubmIgPC0gbmFpdmVCYXllcyhSZXN1bHR+LiwgZGF0YSA9IHRyYWluKQ0KZGVsYXlzLm5iDQoNCnByZWQucHJvYiA8LSBhcy5kYXRhLmZyYW1lKHByZWRpY3QoZGVsYXlzLm5iLCBuZXdkYXRhID0gdGVzdCwgdHlwZSA9ICJyYXciKSkNCnByZWQucHJvYiA8LSAgcm91bmQocHJlZC5wcm9iLCA0KQ0KcHJlZC5wcm9iIDwtIHByZWQucHJvYiAlPiUgc2VsZWN0KCJIaXQiKQ0KbmFtZXMocHJlZC5wcm9iKSA8LSAgInByZWRyYXciDQp0ZXN0IDwtIGNiaW5kKHRlc3QsIHByZWQucHJvYikNCnJtKHByZWQucHJvYikNCg0KcGFyIDwtIGRhdGEuZnJhbWUoIkN1dCBvZmYiID0gMSwgIkFjY3VyYWN5IiA9IDEsICJTZW5zaXRpdml0eSIgPSAxKQ0KaSA9IDANCmogPSAxDQpmb3IoaSBpbiBzZXEoMCwxLDAuMDAxKSl7DQogIHRlc3QgPC0gdGVzdCAlPiUgbXV0YXRlKHByZWQgPSBmYWN0b3IoYXMuY2hhcmFjdGVyKGlmZWxzZShwcmVkcmF3IDwgaSwgIkZsb3AiLCAiSGl0IikpLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCiBwYXJbaiwyXSA8LSAxLW1lYW4odGVzdCRSZXN1bHQgIT0gdGVzdCRwcmVkKQ0KIHRlc3QxIDwtIHRlc3QgJT4lIGZpbHRlcihSZXN1bHQgPT0gIkhpdCIpDQogcGFyW2osM10gPC0gbWVhbih0ZXN0MSRSZXN1bHQgPT0gdGVzdDEkcHJlZCkNCiBwYXJbaiwxXSA8LSBpDQpqIDwtIGorMQ0KaSA8LSBpKzENCnJtKHRlc3QxKQ0KfQ0Kcm0oaSkNCnJtKGopDQoNCmthYmxlKHBhciAlPiUgZmlsdGVyKEFjY3VyYWN5ID09IG1heChBY2N1cmFjeSkpICU+JSBoZWFkKDUpLCBjYXB0aW9uID0gIlRoZSAoSGl0KSBwcm9iYWJpbGl0eSBjdXQgb2ZmIGZvciB0aGUgYmVzdCBhY2N1cmFjeSIpDQoNCnBhciAlPiUgZ2dwbG90KGFlcyh4ID0gYEN1dC5vZmZgLCB5ID0gcGFyJEFjY3VyYWN5KSkrZ2VvbV9wb2ludChzaXplID0gMC4wNSwgY29sb3IgPSAiZ3JheSIpK2dlb21fbGluZSgpK2dlb21faGxpbmUoeWludGVyY2VwdCA9IG1heChwYXIkQWNjdXJhY3kpLCBjb2xvciA9ICJyZWQiKStnZW9tX3ZsaW5lKHhpbnRlcmNlcHQgPSAwLjYxNSwgY29sb3IgPSAicmVkIikrDQogIGxhYnMoeCA9ICJDdXQgT2ZmIHZhbHVlIiwgeSA9ICJBY2N1cmFjeSBvZiBwcmVkaWN0aW9uIiwgdGl0bGUgPSAiQ3V0IG9mZiBWcyBBY2N1cmFjeSIpDQoNCnJtKHBhcikNCg0KdGVzdCA8LSB0ZXN0ICU+JSBtdXRhdGUocHJlZCA9ICBmYWN0b3IoYXMuY2hhcmFjdGVyKGlmZWxzZShwcmVkcmF3IDwgMC42MTUsICJGbG9wIiwgIkhpdCIpKSwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkpDQoNCmNtIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodGVzdCRwcmVkLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSwgZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCmNtDQpuYWIudSA8LSBkYXRhLmZyYW1lKCJNb2RlbCIgPSAiTmFpdmUgQmF5ZXMgKFUpIiwgIkFjY3VyYWN5IiA9IGNtW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCAiU2Vuc2l0aXZpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sICJTcGVjaWZpY2l0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTcGVjaWZpY2l0eSJdXSkNCnJtKGNtKQ0KYGBgDQoNCk1hY2hpbmUgTGVhcm5pbmcgLSBNb2RlbGxpbmcgLSBTVk0NCmBgYHtyIE1MIC0gU1ZNIGZvciBSZWFsIERhdGEsIG1lc3NhZ2U9Riwgd2FybmluZz1GfQ0Kcm0odGVzdCkNCnJtKHRyYWluKQ0Kcm0oZGVsYXlzLm5iKQ0KDQp0cmFpbiA8LSB0cmFpbmluZyAlPiUgc2VsZWN0KC1jKCJSYXRpbmciLCAiUmV2aWV3cyIpKQ0KdHJhaW4kUmVzdWx0IDwtIGZhY3Rvcih0cmFpbiRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQp0ZXN0IDwtIHRlc3RpbmcgJT4lIHNlbGVjdCgtYygiUmF0aW5nIiwgIlJldmlld3MiKSkNCnRlc3QkUmVzdWx0IDwtIGZhY3Rvcih0ZXN0JFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkNCg0Kc2V0LnNlZWQoMTIzKQ0Kc3ZtLmZpdCA8LSBzdm0oUmVzdWx0fi4sIGRhdGE9dHJhaW4sIGtlcm5lbCA9ICJsaW5lYXIiKQ0KDQp0ZXN0JHByZWQgPC0gcHJlZGljdChzdm0uZml0LCBuZXdkYXRhID0gdGVzdFssLTZdLCB0eXBlID0gImNsYXNzIikNCmNtIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodGVzdCRwcmVkLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSwgZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCmNtDQoNCmxpbmVhciA8LSBkYXRhLmZyYW1lKCJNT2RlbCIgPSAiU1ZNIC0gTGluZWFyIChVKSIsICJBY2N1cmFjeSIgPSBjbVtbIm92ZXJhbGwiXV1bWyJBY2N1cmFjeSJdXSwgIlNlbnNpdGl2aXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNlbnNpdGl2aXR5Il1dLCAiU3BlY2lmaWNpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU3BlY2lmaWNpdHkiXV0pDQoNCnJtKHRlc3QpDQpybSh0cmFpbikNCnJtKHN2bS5maXQpDQpybShjbSkNCg0KdHJhaW4gPC0gdHJhaW5pbmcgJT4lIHNlbGVjdCgtYygiUmF0aW5nIiwgIlJldmlld3MiKSkNCnRyYWluJFJlc3VsdCA8LSBmYWN0b3IodHJhaW4kUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKQ0KdGVzdCA8LSB0ZXN0aW5nICU+JSBzZWxlY3QoLWMoIlJhdGluZyIsICJSZXZpZXdzIikpDQp0ZXN0JFJlc3VsdCA8LSBmYWN0b3IodGVzdCRSZXN1bHQsIGxldmVscyA9IGMoIkhpdCIsICJGbG9wIikpDQoNCnNldC5zZWVkKDEyMykNCnN2bS5maXQgPC0gc3ZtKFJlc3VsdH4uLCBkYXRhPXRyYWluLCBrZXJuZWwgPSAicmFkaWFsIikNCg0KdGVzdCRwcmVkIDwtIHByZWRpY3Qoc3ZtLmZpdCwgbmV3ZGF0YSA9IHRlc3RbLC02XSwgdHlwZSA9ICJjbGFzcyIpDQpjbSA8LSBjb25mdXNpb25NYXRyaXgoZmFjdG9yKHRlc3QkcHJlZCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSksIGZhY3Rvcih0ZXN0JFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkpDQpjbQ0KDQpyYWRpYWwgPC0gZGF0YS5mcmFtZSgiTU9kZWwiID0gIlNWTSAtIFJhZGlhbCAoVSkiLCAiQWNjdXJhY3kiID0gY21bWyJvdmVyYWxsIl1dW1siQWNjdXJhY3kiXV0sICJTZW5zaXRpdml0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTZW5zaXRpdml0eSJdXSwgIlNwZWNpZmljaXR5IiA9IGNtW1siYnlDbGFzcyJdXVtbIlNwZWNpZmljaXR5Il1dKQ0KDQpybSh0ZXN0KQ0Kcm0odHJhaW4pDQpybShzdm0uZml0KQ0Kcm0oY20pDQoNCnRyYWluIDwtIHRyYWluaW5nICU+JSBzZWxlY3QoLWMoIlJhdGluZyIsICJSZXZpZXdzIikpDQp0cmFpbiRSZXN1bHQgPC0gZmFjdG9yKHRyYWluJFJlc3VsdCwgbGV2ZWxzID0gYygiSGl0IiwgIkZsb3AiKSkNCnRlc3QgPC0gdGVzdGluZyAlPiUgc2VsZWN0KC1jKCJSYXRpbmciLCAiUmV2aWV3cyIpKQ0KdGVzdCRSZXN1bHQgPC0gZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKQ0KDQpzZXQuc2VlZCgxMjMpDQpzdm0uZml0IDwtIHN2bShSZXN1bHR+LiwgZGF0YT10cmFpbiwga2VybmVsID0gInBvbHlub21pYWwiKQ0KDQp0ZXN0JHByZWQgPC0gcHJlZGljdChzdm0uZml0LCBuZXdkYXRhID0gdGVzdFssLTZdLCB0eXBlID0gImNsYXNzIikNCmNtIDwtIGNvbmZ1c2lvbk1hdHJpeChmYWN0b3IodGVzdCRwcmVkLCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSwgZmFjdG9yKHRlc3QkUmVzdWx0LCBsZXZlbHMgPSBjKCJIaXQiLCAiRmxvcCIpKSkNCmNtDQoNCnBvbHlub21pYWwgPC0gZGF0YS5mcmFtZSgiTU9kZWwiID0gIlNWTSAtIFBvbHlub21pYWwgKFUpIiwgIkFjY3VyYWN5IiA9IGNtW1sib3ZlcmFsbCJdXVtbIkFjY3VyYWN5Il1dLCAiU2Vuc2l0aXZpdHkiID0gY21bWyJieUNsYXNzIl1dW1siU2Vuc2l0aXZpdHkiXV0sICJTcGVjaWZpY2l0eSIgPSBjbVtbImJ5Q2xhc3MiXV1bWyJTcGVjaWZpY2l0eSJdXSkNCg0Kcm0oY20pDQpybSh0ZXN0KQ0Kcm0odHJhaW4pDQpybShzdm0uZml0KQ0KDQpzdm0udSA8LSByYmluZChsaW5lYXIsIHJhZGlhbCwgcG9seW5vbWlhbCkNCnJtKGxpbmVhcikNCnJtKHJhZGlhbCkNCnJtKHBvbHlub21pYWwpDQpuYW1lcyhzdm0udSkgPC0gYygiTW9kZWwiLCAiQWNjdXJhY3kiLCAiU2Vuc2l0aXZpdHkiLCAiU3BlY2lmaWNpdHkiKQ0KYGBgDQoNCg0KDQpGaW5hbCBtb2RlbCBTdW1tYXJ5DQpgYGB7ciBGaW5hbCBNb2RlbCBTdW1tYXJ5LCBtZXNzYWdlPUYsIHdhcm5pbmc9Rn0NCm1vZGVsLmRzIDwtIHJiaW5kKGxvZ2l0LCBkZXQsIHJhZiwgbmFiLCBzdm0pDQpybShsb2dpdCkNCnJtKGRldCkNCnJtKHJhZikNCnJtKG5hYikNCnJtKHN2bSkNCm1vZGVsLnJlYWx0aW1lIDwtIHJiaW5kKGxvZ2l0LnUsIGRldC51LCByYWYudSwgbmFiLnUsIHN2bS51KQ0Kcm0obG9naXQudSkNCnJtKGRldC51KQ0Kcm0ocmFmLnUpDQpybShuYWIudSkNCnJtKHN2bS51KQ0KDQprYWJsZShyYmluZChtb2RlbC5kcywgbW9kZWwucmVhbHRpbWUpLCBjYXB0aW9uID0gIkZpbmFsIG1vZGVsIHJlc3VsdHMgZm9yIGNvbXBsZXRlIGFuZCByZWFsIHRpbWUgZGF0YSIpDQoNCmthYmxlKChtb2RlbC5kcyAlPiUgYXJyYW5nZShkZXNjKEFjY3VyYWN5KSkgJT4lIGhlYWQoMikpLCBjYXB0aW9uID0gIlRvcCAyIG1vZGVscyBmb3IgY29tcGxldGUgZGF0YSIpDQoNCmthYmxlKChtb2RlbC5yZWFsdGltZSU+JSBhcnJhbmdlKGRlc2MoQWNjdXJhY3kpKSAlPiUgaGVhZCgyKSksIGNhcHRpb24gPSAiVG9wIDIgbW9kZWxzIGZvciBkYXRhIHRoYXQgY2FuIGJlIGV4dHJhY3RlZCBpbiByZWFsIHRpbWUiKQ0KYGBgDQoNCmBgYHtyIENvbmNsdXNpb24sIGVjaG8gPSBGLCBldmFsID0gRiwgaW5jbHVkZSA9IEZ9DQoNCmBgYA0KQ29uY2x1c2lvbjoNCg0KSXQgaXMgY2xlYXIgdGhhdCB0aGUgc2l6ZSBvZiB0aGUgYXBwIGFuZCBjYXRlZ29yeSBkZWZpbmUgaWYgdGhlIGFwcCB3b3VsZCBiZSBhIGhpdCBvciBub3QuICANCkZyb20gdGhlIEVEQSBpdCBpcyBjbGVhciB0aGF0IHRoZSBhcHAgbXVzdCBiZSBiZWxvdyAyNSBNQiBhbmQgdGhlIGNhdGVnb3JpZXMgdG8gdGFyZ2V0IHdvdWxkIHZhcnkgZGVwZW5kaW5nIG9uIHRoZSByaXNrIG9uZSB3YW50cyB0byB0YWtlLiAgDQpXaGlsZSBHYW1lLCBGYW1pbHksIGFuZCBDb21tdW5pY2F0aW9uIGdpdmUgYSBjb25zdGFudCByZXdhcmQsIHRoZSBoaWdoZXN0IGNhbiBiZSBnYWluZWQgZnJvbSBDYXRlZ29yaWVzIHRoYXQgYXJlIHlldCB0byBiZSBleHBsb3JlZCBsaWtlIENvbWljcyAoV2hpY2ggYXJlIHBvc2l0aXZlbHkgc2VlbiBidXQgYXJlIGxvdyBpbiBtYXJrZXQpIG9yIEV2ZW50cyAoV2hpY2ggaGF2ZSBhIGh1Z2UgY3JpdGljaXNtIGFuZCBhcmUgbG93IGluIG1hcmtldCkuICANCg0KDQpgYGB7ciBTb3VyY2VzLCBlY2hvID0gRiwgZXZhbCA9IEYsIGluY2x1ZGUgPSBGfQ0KDQpgYGANCg0KU291cmNlczogIA0KaHR0cHM6Ly93d3cua2FnZ2xlLmNvbS9sYXZhMTgvZ29vZ2xlLXBsYXktc3RvcmUtYXBwcyAgDQpodHRwOi8vci1zdGF0aXN0aWNzLmNvLyAgDQpodHRwczovL3VjLXIuZ2l0aHViLmlvLyAgDQpodHRwczovL3d3dy5yLWJsb2dnZXJzLmNvbS8gIA0KaHR0cHM6Ly9tZWRpdW0uY29tL2FuYWx5dGljcy12aWRoeWEvYS1ndWlkZS10by1tYWNoaW5lLWxlYXJuaW5nLWluLXItZm9yLWJlZ2lubmVycy1wYXJ0LTUtNGMwMGYyMzY2YjkwICANCmh0dHBzOi8vZW4ucHJvZnQubWUvMjAxNi8xMS85L2NsYXNzaWZpY2F0aW9uLXVzaW5nLWRlY2lzDQppb24tdHJlZXMtci8gIA0KaHR0cHM6Ly93d3cuZ29ybWFuYWx5c2lzLmNvbS9ibG9nL2RlY2lzaW9uLXRyZWVzLWluLXItdXMNCmluZy1ycGFydC8gIA0KaHR0cDovL3d3dy5zdGhkYS5jb20vZW5nbGlzaC9hcnRpY2xlcy8zNS1zdGF0aXN0aWNhbC1tYWNoaW5lLWxlYXJuaW5nLWVzc2VudGlhbHMvMTQxLWNhcnQtbW9kZWwtZGVjaXNpb24tdHJlZS1lc3NlbnRpYWxzLyAgDQpodHRwczovL2RhdGFhc3BpcmFudC5jb20vMjAxNy8wMi8wMy9kZWNpc2lvbi10cmVlLWNsYXNzaWZpZXItaW1wbGVtZW50YXRpb24taW4tci8gIA0KaHR0cHM6Ly93d3cuYmxvcGlnLmNvbS9ibG9nLzIwMTcvMDQvYS12ZXJ5LWJhc2ljLWludHJvZHVjdGlvbi10by1yYW5kb20tZm9yZXN0cy11c2luZy1yLyAgDQpodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvICANCmh0dHBzOi8vd3d3LmRhdGFjYW1wLmNvbS8gIA0KDQpJbnRyb2R1Y3Rpb24gdG8gRGF0YSBNaW5pbmcgIA0KQm9vayBieSBNaWNoYWVsIFN0ZWluYmFjaCwgUGFuZy1OaW5nIFRhbiwgYW5kIFZpcGluIEt1bWFyDQoNCkRhdGEgTWluaW5nIGZvciBCdXNpbmVzcyBBbmFseXRpY3M6IENvbmNlcHRzLCBUZWNobmlxdWVzLCBhbmQgQXBwbGljYXRpb25zIGluIFIgICANCkJvb2sgYnkgR2FsaXQgU2htdWVsaSAsIFBldGVyIEMuIEJydWNlLCBJbmJhbCBZYWhhdiwgTml0aW4gUi4gUGF0ZWwsDQpLZW5uZXRoIEMuIExpY2h0ZW5kYWhsIEpyLiANCg==