1.0 Introduction

This notebook analyzes a series of tweets related to the term “java”. Each tweet was classified by a human into one of several categories:

  • java - posts seeking Java developers

  • learning - learning opportunities or announcements having to do with Java programming

  • opinion - an opinion or a comment on Java programming

  • help - posts seeking help with Java programming

  • coffee - posts having to do with coffee (java is a common nickname for coffee)

  • Indonesia - posts having to do with the island of Java in Indonesia

  • irrelevant - posts that could be understood but could not be classified into any of the above

We will train a Support Vector Machine classifier to see how well it could predict the labels of unseen tweets.

2. Reading the data

Loading the classified tweets.

tweets = read.csv("tweets_java_two_days_rated.csv", stringsAsFactors=FALSE)
Warning message:
In scan(file, what, nmax, sep, dec, quote, skip, nlines, na.strings,  :
  EOF within quoted string
str(tweets)
'data.frame':   2027 obs. of  18 variables:
 $ X            : int  1 2 3 4 5 6 7 8 9 10 ...
 $ text         : chr  "Si sos de los mejores <U+2615><U+FE0F> JAVA DEVELOPERS <U+2615><U+FE0F>  ssr/sr. sumate al #TeamVates \nrrhh@vates.com  https:/"| __truncated__ "java<U+3067><U+30DD><U+30A4><U+30F3><U+30C8><U+306B><U+306A><U+308B><U+3082><U+306E><U+3092><U+307E><U+3068><U+3081><U+3066><U+"| __truncated__ "A m�sica do Rappa j� dizia: \"Me abrace e me d� um beijo, fa�a um filho comigo, mas n�o me deixe programar em Java nu"| __truncated__ "@jealousRD <U+C774> <U+C774><U+C0C1><U+D55C> <U+AE30><U+C6B4><U+B9CC> <U+B118><U+CE58><U+B294> <U+C790><U+C2DD><U+C774> <U+AE30"| __truncated__ ...
 $ class        : chr  "job" "" "" "" ...
 $ favorited    : chr  "FALSE" "0" "0" "0" ...
 $ favoriteCount: chr  "0" NA NA "jealousRD" ...
 $ replyToSN    : chr  NA "2017-08-15 15:10:03" "2017-08-15 15:10:03" "2017-08-15 15:10:00" ...
 $ created      : chr  "2017-08-15 15:10:05" "FALSE" "FALSE" "FALSE" ...
 $ truncated    : chr  "FALSE" NA NA "897475237205336000" ...
 $ replyToSID   : num  NA 8.97e+17 8.97e+17 8.97e+17 NA ...
 $ id           : num  8.97e+17 NA NA 3.46e+09 8.97e+17 ...
 $ replyToUID   : chr  NA "<a href=\"http://twittbot.net/\" rel=\"nofollow\">twittbot.net</a>" "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>" "<a href=\"http://twitter.com/download/android\" rel=\"nofollow\">Twitter for Android</a>" ...
 $ statusSource : chr  "<a href=\"http://www.hootsuite.com\" rel=\"nofollow\">Hootsuite</a>" "hjFGlLb8yjtrGNg" "senhor_caveira" "Te_JaVa__" ...
 $ screenName   : chr  "Vates_SA" "0" "0" "0" ...
 $ retweetCount : chr  "0" "FALSE" "FALSE" "FALSE" ...
 $ isRetweet    : logi  FALSE FALSE FALSE FALSE FALSE FALSE ...
 $ retweeted    : chr  "FALSE" NA NA NA ...
 $ longitude    : num  NA NA NA NA NA NA NA NA NA NA ...
 $ latitude     : num  NA NA NA NA NA NA NA NA NA NA ...

We are only interested in the text and the classification of the tweets for the time being, so we discard the rest.

tweets <- tweets[,c("text", "class")]
str(tweets)
'data.frame':   2027 obs. of  2 variables:
 $ text : chr  "Si sos de los mejores <U+2615><U+FE0F> JAVA DEVELOPERS <U+2615><U+FE0F>  ssr/sr. sumate al #TeamVates \nrrhh@vates.com  https:/"| __truncated__ "java<U+3067><U+30DD><U+30A4><U+30F3><U+30C8><U+306B><U+306A><U+308B><U+3082><U+306E><U+3092><U+307E><U+3068><U+3081><U+3066><U+"| __truncated__ "A m�sica do Rappa j� dizia: \"Me abrace e me d� um beijo, fa�a um filho comigo, mas n�o me deixe programar em Java nu"| __truncated__ "@jealousRD <U+C774> <U+C774><U+C0C1><U+D55C> <U+AE30><U+C6B4><U+B9CC> <U+B118><U+CE58><U+B294> <U+C790><U+C2DD><U+C774> <U+AE30"| __truncated__ ...
 $ class: chr  "job" "" "" "" ...

Let’s remove the unclassified tweets

tweets <- tweets[tweets$class != "",]
str(tweets)
'data.frame':   838 obs. of  2 variables:
 $ text : chr  "Si sos de los mejores <U+2615><U+FE0F> JAVA DEVELOPERS <U+2615><U+FE0F>  ssr/sr. sumate al #TeamVates \nrrhh@vates.com  https:/"| __truncated__ "How to implement #Builder #design #pattern in #Java https://t.co/RzGsrtcDMT" "How Memory Leaks Happen in a Java Application | CloudExpo #JVM #Java #Virtualization https://t.co/zPLATAhdHB" "Kotlin 1.1.4 released! more info: https://t.co/BoyevQFJOC programming #kotlin #java #jvm  via @kotlin https://t.co/2DQ4PHfEo9" ...
 $ class: chr  "job" "learning" "learning" "learning" ...

3.0 Pre-processing the data

We want to streamline the dataset by discarding redundancies and certain words that don’t aid in classifying a tweet. We first load two libraries we will be using for that purpose: the text mining library tm and the word stemmer SnowballC.

library(tm)
library(SnowballC)

3.1 Creating a corpus of tweets

Create the word corpus, which is basically a list of the documents (tweets). Actually, it’s a list of lists: each corpus’ sub-list has a “content” component, which is the actual tweet, and a “meta” component, some metadata.

# Create corpus
corpus = Corpus(VectorSource(tweets$text))
# Look at the first tweet's text
corpus[[1]]$content
[1] "Si sos de los mejores <U+2615><U+FE0F> JAVA DEVELOPERS <U+2615><U+FE0F>  ssr/sr. sumate al #TeamVates \nrrhh@vates.com  https://t.co/ge08rmXEJ2 https://t.co/cDecCVBeU1"

3.2 Converting tweets to lowercase

We want to convert all the text of each tweet to lowercase so the classifier won’t have to distinguish between “Java” and “java”, for example.

# Convert to lower-case
# tm_map() applies the function tolower() to the corpus
corpus = tm_map(corpus, tolower)
# https://discuss.analyticsvidhya.com/t/error-inherits-doc-textdocument-is-not-true-in-r/1078/4
corpus <- tm_map(corpus, PlainTextDocument)
# Look at the first tweet's text
corpus[[1]]$content
[1] "si sos de los mejores <u+2615><u+fe0f> java developers <u+2615><u+fe0f>  ssr/sr. sumate al #teamvates \nrrhh@vates.com  https://t.co/ge08rmxej2 https://t.co/cdeccvbeu1"

3.3 Removing punctuation and stopwords

Punctuation is not helpful to classify text, so we can discard it.

# Remove punctuation
# tm_map() applies the function removePunctuation() to the corpus
corpus = tm_map(corpus, removePunctuation)
# Look at the first tweet's text
corpus[[1]]$content
[1] "si sos de los mejores u2615ufe0f java developers u2615ufe0f  ssrsr sumate al teamvates \nrrhhvatescom  httpstcoge08rmxej2 httpstcocdeccvbeu1"

Likewise, English stopwords such as articles (“the”, “a”, “an”), pronouns (“we”, “she”, “it”, …), the verb “to be” and its conjugations (“is”, “was”, “were”, …), etc. do not aid in classification. We will also remove the term “java” from each tweet, since it’s common to all tweets and therefore won’t help us either.

# Remove English stopwords
# tm_map() applies the function removeWords() and its arguments to the corpus
corpus = tm_map(corpus, removeWords, c("java", stopwords("english")))
# Look at the first tweet's text
corpus[[1]]$content
[1] "si sos de los mejores u2615ufe0f  developers u2615ufe0f  ssrsr sumate al teamvates \nrrhhvatescom  httpstcoge08rmxej2 httpstcocdeccvbeu1"

3.4 Stemming the tweets

This streamlines the tweets further by removing words’ inflections. E.g., “I tried to…”, “I have been trying to…”, “I will try to…” all have conjugations of the verb “to try”, but we hardly need to include all three of them as separate features. Another example would be word plurals, such as “job” and “jobs”.

# Stem document 
# tm_map() applies the function stemDocument() to the corpus
corpus = tm_map(corpus, stemDocument)
# Look at the first tweet's text
corpus[[1]]$content
[1] "si sos de los mejor u2615ufe0f  develop u2615ufe0f  ssrsr sumat al teamvat \nrrhhvatescom  httpstcoge08rmxej2 httpstcocdeccvbeu1"

3.5 Creating the Document-Term Matrix

This will create a matrix where each row is a document (tweet) and each column is a term, i.e., a word. Each entry is a frequency of a given word in a given tweet

# Create matrix
frequencies = DocumentTermMatrix(corpus)
frequencies
<<DocumentTermMatrix (documents: 838, terms: 3600)>>
Non-/sparse entries: 7066/3009734
Sparsity           : 100%
Maximal term length: 295
Weighting          : term frequency (tf)

3.6 Dealing with DTM sparsity

The frequencies DTM is very sparse, which means almost all its entries are 0. We have a matrix with hundreds of thousands of items and only a few thousand of them are non-zero. We can reduce the size of the matrix by requiring that a word (term) appears in at least a certain percentage of the tweets for it to be included. If it doesn’t, we can discard that column

# Remove sparse terms
# The sparse argument in removeSparseTerms():
# If we set it to 0.99, that means we want to keep terms (columns) that appear
# in 1% or more of the documents (tweets in this case). If we set it to 0.995,
# we want to keep terms that appear in 0.5% or more of the tweets.
sparse = removeSparseTerms(frequencies, 0.99)
sparse
<<DocumentTermMatrix (documents: 838, terms: 99)>>
Non-/sparse entries: 1923/81039
Sparsity           : 98%
Maximal term length: 13
Weighting          : term frequency (tf)

We have reduced the size of the DTM by over an order of magnitude, from 3600 terms to 99.

3.7 Converting DTM to dataframe and final tidying up

Convert sparse from DTM to a dataframe

# Convert to a data frame
tweetsSparse = as.data.frame(as.matrix(sparse))
class(tweetsSparse)
[1] "data.frame"

make.names() makes all variable names R-friendly. This is done specially for numbers, which can’t be used as variables. So for example, make.names("5") returns “X5”.

# Make all terms R-friendly
colnames(tweetsSparse) = make.names(colnames(tweetsSparse))

Convert the classification column \(class\) from character to factor.

# Converting from character to factor
tweetsSparse$class = as.factor(tweets$class)

4.0 Data visualization: word clouds

Before building the SVM classifier, we can visualize the term (word stem) frequencies of the tweets’ classes.

# http://www.sthda.com/english/wiki/text-mining-and-word-cloud-fundamentals-in-r-5-simple-steps-you-should-know
layout(matrix(c(rep(1,3), rep(2,3), rep(3,3), rep(4,3), rep(5,2), rep(6,2), rep(7,2)), ncol = 6, byrow=TRUE))
#layout.show(4)
par(mar=rep(0, 4))
set.seed(5678)
library(wordcloud)
# Job tweets' word cloud
tweetsSparseJobs <- tweetsSparse[tweetsSparse$class == "job",]
tweetsSparseJobsMatrix <- as.matrix(tweetsSparseJobs[, !names(tweetsSparseJobs) %in% c("class")])
freq_table_jobs <- sort(colSums(tweetsSparseJobsMatrix),decreasing=TRUE)
freq_df_jobs <- data.frame(word = names(freq_table_jobs), freq=freq_table_jobs)
wordcloud(words = freq_df_jobs$word, freq = freq_df_jobs$freq, min.freq = 1, 
          max.words=200, random.order=FALSE, rot.per=0.35, 
          colors=brewer.pal(8, "Dark2"), scale = c(7, 0.7))
text(x=0.5, y=1, "Jobs word cloud", cex = 1.5, font = 2)
# Learning tweets' word cloud
tweetsSparseLearning <- tweetsSparse[tweetsSparse$class == "learning",]
tweetsSparseLearningMatrix <- as.matrix(tweetsSparseLearning[, !names(tweetsSparseLearning) %in% c("class")])
freq_table_learning <- sort(colSums(tweetsSparseLearningMatrix),decreasing=TRUE)
freq_df_learning <- data.frame(word = names(freq_table_learning), freq=freq_table_learning)
wordcloud(words = freq_df_learning$word, freq = freq_df_learning$freq, min.freq = 1, 
          max.words=200, random.order=FALSE, rot.per=0.35, 
          colors=brewer.pal(8, "Dark2"), scale = c(5, 0.5))
text(x=0.5, y=1, "Learning word cloud", cex = 1.5, font = 2)
# Opinion tweets' word cloud
tweetsSparseOpinion <- tweetsSparse[tweetsSparse$class == "opinion",]
tweetsSparseOpinionMatrix <- as.matrix(tweetsSparseOpinion[, !names(tweetsSparseOpinion) %in% c("class")])
freq_table_opinion <- sort(colSums(tweetsSparseOpinionMatrix),decreasing=TRUE)
freq_df_opinion <- data.frame(word = names(freq_table_opinion), freq=freq_table_opinion)
wordcloud(words = freq_df_opinion$word, freq = freq_df_opinion$freq, min.freq = 1, 
          max.words=200, random.order=FALSE, rot.per=0.35, 
          colors=brewer.pal(8, "Dark2"), scale = c(4.0, 0.4))
text(x=0.5, y=1, "Opinion word cloud", cex = 1.5, font = 2)
# Indonesia tweets' word cloud
tweetsSparseIndonesia <- tweetsSparse[tweetsSparse$class == "Indonesia",]
tweetsSparseIndonesiaMatrix <- as.matrix(tweetsSparseIndonesia[, !names(tweetsSparseIndonesia) %in% c("class")])
freq_table_Indonesia <- sort(colSums(tweetsSparseIndonesiaMatrix),decreasing=TRUE)
freq_df_Indonesia <- data.frame(word = names(freq_table_Indonesia), freq=freq_table_Indonesia)
wordcloud(words = freq_df_Indonesia$word, freq = freq_df_Indonesia$freq, min.freq = 1, 
          max.words=200, random.order=FALSE, rot.per=0.35, 
          colors=brewer.pal(8, "Dark2"), scale = c(8, 0.8))
text(x=0.5, y=1, "Indonesia word cloud", cex = 1.5, font = 2)
# Coffee tweets' word cloud
tweetsSparseCoffee <- tweetsSparse[tweetsSparse$class == "coffee",]
tweetsSparseCoffeeMatrix <- as.matrix(tweetsSparseCoffee[, !names(tweetsSparseCoffee) %in% c("class")])
freq_table_coffee <- sort(colSums(tweetsSparseCoffeeMatrix),decreasing=TRUE)
freq_df_coffee <- data.frame(word = names(freq_table_coffee), freq=freq_table_coffee)
wordcloud(words = freq_df_coffee$word, freq = freq_df_coffee$freq, min.freq = 1, 
          max.words=200, random.order=FALSE, rot.per=0.35, 
          colors=brewer.pal(8, "Dark2"), scale = c(6, 0.6))
text(x=0.5, y=1, "Coffee word cloud", cex = 1.5, font = 2)
# Help tweets' word cloud
tweetsSparseHelp <- tweetsSparse[tweetsSparse$class == "help",]
tweetsSparseHelpMatrix <- as.matrix(tweetsSparseHelp[, !names(tweetsSparseHelp) %in% c("class")])
freq_table_help <- sort(colSums(tweetsSparseHelpMatrix),decreasing=TRUE)
freq_df_help <- data.frame(word = names(freq_table_help), freq=freq_table_help)
wordcloud(words = freq_df_help$word, freq = freq_df_help$freq, min.freq = 1, 
          max.words=200, random.order=FALSE, rot.per=0.35, 
          colors=brewer.pal(8, "Dark2"), scale = c(5, 0.5))
text(x=0.5, y=1, "Help word cloud", cex = 1.5, font = 2)
# Irrelevant tweets' word cloud
tweetsSparseIrrel <- tweetsSparse[tweetsSparse$class == "irrelevant",]
tweetsSparseIrrelMatrix <- as.matrix(tweetsSparseIrrel[, !names(tweetsSparseIrrel) %in% c("class")])
freq_table_irrel <- sort(colSums(tweetsSparseIrrelMatrix),decreasing=TRUE)
freq_df_irrel <- data.frame(word = names(freq_table_irrel), freq=freq_table_irrel)
wordcloud(words = freq_df_irrel$word, freq = freq_df_irrel$freq, min.freq = 1, 
          max.words=200, random.order=FALSE, rot.per=0.35, 
          colors=brewer.pal(8, "Dark2"), scale = c(5, 0.5))
text(x=0.5, y=1, "Irrelevant word cloud", cex = 1.5, font = 2)

There are several terms that appear frequently in more than one category. For example, “develop” appears often in tweets related to both jobs and learning opportunities. Likewise, “python” and “javascript” commonly show up in both opinion and learning tweets.

5.0 Building the SVM classifier

Now we are ready to build the classifier.

5.1 Splitting into training and testing sets

We will split the data 70/30 into training and testing sets

# Split the data
library(caTools)
set.seed(5678)
split = sample.split(tweetsSparse$class, SplitRatio = 0.7)
trainSparse = subset(tweetsSparse, split==TRUE)
testSparse = subset(tweetsSparse, split==FALSE)
cat("Dimensions of training dataset: ", dim(trainSparse), "\nDimensions of test dataset: ", dim(testSparse))
Dimensions of training dataset:  585 100 
Dimensions of test dataset:  253 100

5.2 An SVM classifier with an RBF kernel

We will train the e1071 library’s implementation of SVM using a Radial Basis Function kernel. Please see Section 9.3.2 of ISLR for an overview of the RBF kernel. We will follow the advice of the folks behind the LIBSVM algorithm that is implemented by the e1071 library and try the RBF kernel first.

5.2.1 An SVM with RBF kernel example

Most of the examples of support vector classification, whether using linear, polynomial, or RBF kernels, involve two classes. I wanted to visualize how it worked in a case where the data had more than two classes, since in our case we have seven. I created an artificial four-class data sample where the data is definitely not linearly separable, and tried to train the SVM with RBF kernel on it. Here is the made-up example data:

set.seed(5678)
x41 <- runif(200, min = -3, max = 3)
x42 <- runif(200, min = -3, max = 3)
y41 <- ifelse(sqrt(x41^2 + x42^2) < 1.0, 1, ifelse(sqrt(x41^2 + x42^2) > 1.0 & sqrt(x41^2 + x42^2) < 2.0, 2, ifelse(sqrt(x41^2 + x42^2) > 2.0 & sqrt(x41^2 + x42^2) < 3.0, 3, 4)))
plot(x41, x42, col = ifelse(sqrt(x41^2 + x42^2) < 1.0, "red", ifelse(sqrt(x41^2 + x42^2) > 1.0 & sqrt(x41^2 + x42^2) < 2.0, "green", ifelse(sqrt(x41^2 + x42^2) > 2.0 & sqrt(x41^2 + x42^2) < 3.0, "blue", "gray"))), pch = 19)
# Plotting circles
# https://stackoverflow.com/questions/22265704/drawing-circle-in-r
radius <- c(1:3)
theta <- seq(0, 2 * pi, length = 200)
# draw the circles
lines(x = radius[1] * cos(theta), y = radius[1] * sin(theta))
lines(x = radius[2] * cos(theta), y = radius[2] * sin(theta))
lines(x = radius[3] * cos(theta), y = radius[3] * sin(theta))

The data has been arbitrarily classified inside or outside either rings or circles. We only have 2 predictors so we can visualize it. Here is the SVM-RBF implementation. We will follow the advice of the folks behind the LIBSVM algorithm that is implemented by the e1071 library and use cross-validation to find the best parameters. We can tune the model using tune.svm(), which finds optimal values of \(\gamma\) and \(C\) using 10-fold cross-validation. Here we are using values of \(\gamma\) from \(10^{-3}\) to \(10^{-1}\), with \(C\) values of \(1\), \(10\), and \(100\).

y41 <- as.factor(y41)
my_df4 <- data.frame(x42, x41, y41)
set.seed(5678)
library(e1071)
my_model4_tuned <- tune.svm(y41~., data = my_df4, kernel = "radial", gamma = 10^(-3:-1), cost = 10^(0:2))
gamma_best <- my_model4_tuned$best.parameters$gamma
cost_best <- my_model4_tuned$best.parameters$cost
my_model4 <- svm(y41 ~ ., data = my_df4, kernel = "radial", gamma = gamma_best, cost = cost_best)
plot(my_model4, my_df4, xlim = c(-3,3), ylim = c(-3,3))

5.2.2 Training the SVM with RBF kernel on the twitter data

We can follow the same procedure to train the SVM on the twitter data. Here we will not be able to visualize the data since the there are many predictors.

set.seed(5678)
tuned <- tune.svm(class~., data = trainSparse, gamma = 10^(-6:-1), cost = 10^(0:2))
gamma_best <- tuned$best.parameters$gamma
cost_best <- tuned$best.parameters$cost
model.tuned <- svm(class~., data = trainSparse, gamma = gamma_best, cost = cost_best)

Let’s see how the tuned SVM model performs on the test data.

svm.tuned.pred <- predict(model.tuned, newdata=testSparse, type="class")
svm.tuned.table <- table(testSparse$class, svm.tuned.pred)
svm.tuned.table
            svm.tuned.pred
             coffee help Indonesia irrelevant job learning opinion
  coffee          6    0         0          0   0        8       0
  help            0    1         0          0   0        3       1
  Indonesia       0    0         3          0   0        5       1
  irrelevant      0    0         0          6   0        3       0
  job             3    0         0          2  83        7       0
  learning        1    2         0          0   9       59       4
  opinion         4    0         0          0   2       25      15

Performance of the tuned model on the test set

accuracy_tuned_svm <- sum(diag(svm.tuned.table)) / sum(svm.tuned.table)
cat("Accuracy of tuned SVM:", accuracy_tuned_svm)
Accuracy of tuned SVM: 0.6837945

The accuracy of the SVM model is good. It does well for the two most frequent tweet categories, jobs and learning, but it tends to misclassify the opinion tweets as learning ones. It correctly classified only about a third of the opinion tweets. Since the opinion tweets are the third most frequently seen class after jobs and learning, the model’s accuracy degraded significantly. The model also was only able to correctly classify only one out of five help tweets, and three out of nine Indonesia tweets.

5.3 An SVM classifier with a linear kernel

It has been argued that linear kernels might be better suited to text classification than RBF kernels. Linear kernels are less computationally expensive, so they are worth a try.

5.3.1 An SVM with linear kernel example

Here again I wanted to see what an SVM trained on multi-class data would look like, this time using a linear kernel. Again artificially generated data, now linearly separable, was generated.

set.seed(5678)
x11 <- rnorm(100)
x22 <- rnorm(100)
y1 <- ifelse(x22 <= x11 & x22 >= -x11, 1, ifelse(x22 >= x11 & x22 >= -x11, 2, ifelse(x22 <= -x11 & x22 >= x11, 3, 4)))
plot(x11, x22, col = ifelse(x22 <= x11 & x22 >= -x11, "red", ifelse(x22 >= x11 & x22 >= -x11, "green", ifelse(x22 <= -x11 & x22 >= x11, "blue", "gray"))), pch = 19, xlim = c(-2,2), ylim = c(-2,2))
lines(x11, -x11)
lines(x11, x11)

Now let’s train the SVM with linear kernel

y1 <- as.factor(y1)
my_df2 <- data.frame(x22, x11, y1)
my_model2 <- svm(y1 ~ ., data = my_df2, kernel = "linear")
plot(my_model2, my_df2)

Great! The plotting of the SVM model is a little jagged, but still.

5.3.2 Training the SVM with linear kernel on the twitter data

Now let’s see how the linear kernel does on the twitter data. Again, we won’t be able to see any nifty plot here.

set.seed(5678)
tuned.linear <- tune.svm(class~., kernel = "linear", data = trainSparse, cost = 10^(-2:2))
best_cost_linear <- tuned.linear$best.parameters$cost
model.linear.tuned <- svm(class~., data = trainSparse, kernel = "linear", cost = best_cost_linear)

Let’s see how the tuned SVM model performs on the test data.

svm.tuned.linear.pred <- predict(model.linear.tuned, newdata=testSparse, type="class")
svm.tuned.linear.table <- table(testSparse$class, svm.tuned.linear.pred)
svm.tuned.linear.table
            svm.tuned.linear.pred
             coffee help Indonesia irrelevant job learning opinion
  coffee          5    0         0          0   0        9       0
  help            0    0         0          0   0        4       1
  Indonesia       0    0         3          0   0        5       1
  irrelevant      0    0         0          5   0        4       0
  job             0    0         0          2  86        7       0
  learning        1    1         0          0   8       60       5
  opinion         2    0         0          0   2       26      16

Performance of the tuned linear SVM model on the test set

accuracy_tuned_linear_svm <- sum(diag(svm.tuned.linear.table)) / sum(svm.tuned.linear.table)
cat("Accuracy of tuned linear SVM:", accuracy_tuned_linear_svm)
Accuracy of tuned linear SVM: 0.6916996

So the performance of the linear kernel is comparable to that of the RBF kernel, if not a little better. Since it is also less computationally onerous, it might be the preferred way to go.

6.0 Summary

We have tried an RBF-SVM and a linear-SVM classifier on a dataset of tweets that have been categorized into seven classes. The accuracy of both classifiers is just shy of \(70\%\). Since linear-SVM is more computationally inexpensive, we would prefer it. If we had been a little less ambitious and lumped the “job”, “learning”, “help”, and “opinion” tweets into a single class called “programming”, either SVM would have achieved an accuracy in the high eighties or low nineties. For example, the linear SVM would have had \(25\) misclassified tweets in the test set, out of \(253\) tweets.

7.0 References

  1. Bertsimas, D., O’Hair, A. The Analytics Edge. Spring 2014. edX.org.

  2. Chiu Yu-Wei. Machine Learning with R Cookbook. Birmingham: Packt Publishing, 2015, PDF.

  3. Hsu Chih-Wei, Chih-Chung Chang, and Chih-Jen Lin. A Practical Guide to Support Vector Classification

  4. adityashrm21 and anon. Error: inherits(doc, “TextDocument”) is not TRUE in R.

  5. STHDA. Text mining and word cloud fundamentals in R : 5 simple steps you should know.

  6. knb and Andrie. R: add title to wordcloud graphics / png.

  7. Sandra Schlichting and bnaul. Plot two graphs in same plot in R

  8. Mona Jalal and Gregor. drawing circle in R

  9. Ghose, Abhishek. Support Vector Machine (SVM) Tutorial

  10. Jcrow06 and tim riffe. R color scatter plot points based on values

  11. Charles Martin. KERNELS PART 1: WHAT IS AN RBF KERNEL? REALLY?

LS0tDQp0aXRsZTogIkNsYXNzaWZ5aW5nIHR3ZWV0cyB1c2luZyBTVk0iDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOg0KICAgIHRvYzogdHJ1ZQ0KICAgIHRvY19kZXB0aDogNQ0KICAgIHRvY19mbG9hdDogdHJ1ZQ0KLS0tDQoNCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+DQoNCmJvZHksIHRkIHsNCiAgIGZvbnQtc2l6ZTogMThweDsNCn0NCmgxIHsNCiAgZm9udC1zaXplOiAzMnB4Ow0KICBmb250LXdlaWdodDogYm9sZDsNCn0NCmgyIHsNCiAgZm9udC1zaXplOiAyOHB4Ow0KICBmb250LXdlaWdodDogYm9sZDsNCn0NCmgzIHsNCiAgZm9udC1zaXplOiAyNHB4Ow0KICBmb250LXdlaWdodDogYm9sZDsNCn0NCmg0IHsNCiAgZm9udC1zaXplOiAyMHB4Ow0KICBmb250LXdlaWdodDogYm9sZDsNCn0NCmNvZGUucnsNCiAgZm9udC1zaXplOiAxNnB4Ow0KfQ0KcHJlIHsNCiAgZm9udC1zaXplOiAxNnB4DQp9DQo8L3N0eWxlPg0KDQojIyAxLjAgSW50cm9kdWN0aW9uDQoNClRoaXMgbm90ZWJvb2sgYW5hbHl6ZXMgYSBzZXJpZXMgb2YgdHdlZXRzIHJlbGF0ZWQgdG8gdGhlIHRlcm0gImphdmEiLiBFYWNoIHR3ZWV0IHdhcyBjbGFzc2lmaWVkIGJ5IGEgaHVtYW4gaW50byBvbmUgb2Ygc2V2ZXJhbCBjYXRlZ29yaWVzOg0KDQotIGphdmEgLSBwb3N0cyBzZWVraW5nIEphdmEgZGV2ZWxvcGVycw0KDQotIGxlYXJuaW5nIC0gbGVhcm5pbmcgb3Bwb3J0dW5pdGllcyBvciBhbm5vdW5jZW1lbnRzIGhhdmluZyB0byBkbyB3aXRoIEphdmEgcHJvZ3JhbW1pbmcNCg0KLSBvcGluaW9uIC0gYW4gb3BpbmlvbiBvciBhIGNvbW1lbnQgb24gSmF2YSBwcm9ncmFtbWluZw0KDQotIGhlbHAgLSBwb3N0cyBzZWVraW5nIGhlbHAgd2l0aCBKYXZhIHByb2dyYW1taW5nDQoNCi0gY29mZmVlIC0gcG9zdHMgaGF2aW5nIHRvIGRvIHdpdGggY29mZmVlIChqYXZhIGlzIGEgY29tbW9uIG5pY2tuYW1lIGZvciBjb2ZmZWUpDQoNCi0gSW5kb25lc2lhIC0gcG9zdHMgaGF2aW5nIHRvIGRvIHdpdGggdGhlIGlzbGFuZCBvZiBKYXZhIGluIEluZG9uZXNpYQ0KDQotIGlycmVsZXZhbnQgLSBwb3N0cyB0aGF0IGNvdWxkIGJlIHVuZGVyc3Rvb2QgYnV0IGNvdWxkIG5vdCBiZSBjbGFzc2lmaWVkIGludG8gYW55IG9mIHRoZSBhYm92ZQ0KDQpXZSB3aWxsIHRyYWluIGEgW1N1cHBvcnQgVmVjdG9yIE1hY2hpbmVdKGh0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZUhzRXJsUEpXVVUmaGQ9MSkgY2xhc3NpZmllciB0byBzZWUgaG93IHdlbGwgaXQgY291bGQgcHJlZGljdCB0aGUgbGFiZWxzIG9mIHVuc2VlbiB0d2VldHMuDQoNCiMjIDIuIFJlYWRpbmcgdGhlIGRhdGENCg0KTG9hZGluZyB0aGUgY2xhc3NpZmllZCB0d2VldHMuDQpgYGB7cn0NCnR3ZWV0cyA9IHJlYWQuY3N2KCJ0d2VldHNfamF2YV90d29fZGF5c19yYXRlZC5jc3YiLCBzdHJpbmdzQXNGYWN0b3JzPUZBTFNFKQ0KDQpzdHIodHdlZXRzKQ0KYGBgDQoNCldlIGFyZSBvbmx5IGludGVyZXN0ZWQgaW4gdGhlIHRleHQgYW5kIHRoZSBjbGFzc2lmaWNhdGlvbiBvZiB0aGUgdHdlZXRzIGZvciB0aGUgdGltZSBiZWluZywgc28gd2UgZGlzY2FyZCB0aGUgcmVzdC4NCmBgYHtyfQ0KdHdlZXRzIDwtIHR3ZWV0c1ssYygidGV4dCIsICJjbGFzcyIpXQ0Kc3RyKHR3ZWV0cykNCmBgYA0KDQpMZXQncyByZW1vdmUgdGhlIHVuY2xhc3NpZmllZCB0d2VldHMNCmBgYHtyfQ0KdHdlZXRzIDwtIHR3ZWV0c1t0d2VldHMkY2xhc3MgIT0gIiIsXQ0Kc3RyKHR3ZWV0cykNCmBgYA0KDQojIyAzLjAgUHJlLXByb2Nlc3NpbmcgdGhlIGRhdGENCg0KV2Ugd2FudCB0byBzdHJlYW1saW5lIHRoZSBkYXRhc2V0IGJ5IGRpc2NhcmRpbmcgcmVkdW5kYW5jaWVzIGFuZCBjZXJ0YWluIHdvcmRzIHRoYXQgZG9uJ3QgYWlkIGluIGNsYXNzaWZ5aW5nIGEgdHdlZXQuIFdlIGZpcnN0IGxvYWQgdHdvIGxpYnJhcmllcyB3ZSB3aWxsIGJlIHVzaW5nIGZvciB0aGF0IHB1cnBvc2U6IHRoZSB0ZXh0IG1pbmluZyBsaWJyYXJ5IFsqKnRtKipdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy90bS90bS5wZGYpIGFuZCB0aGUgd29yZCBzdGVtbWVyIFsqKlNub3diYWxsQyoqXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvU25vd2JhbGxDL1Nub3diYWxsQy5wZGYpLg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRtKQ0KbGlicmFyeShTbm93YmFsbEMpDQpgYGANCg0KIyMjIDMuMSBDcmVhdGluZyBhIGNvcnB1cyBvZiB0d2VldHMNCg0KQ3JlYXRlIHRoZSB3b3JkIGNvcnB1cywgd2hpY2ggaXMgYmFzaWNhbGx5IGEgbGlzdCBvZiB0aGUgZG9jdW1lbnRzICh0d2VldHMpLiBBY3R1YWxseSwgaXQncyBhIGxpc3Qgb2YgbGlzdHM6IGVhY2ggY29ycHVzJyBzdWItbGlzdCBoYXMgYSAiY29udGVudCIgY29tcG9uZW50LCB3aGljaCBpcyB0aGUgYWN0dWFsIHR3ZWV0LCBhbmQgYSAibWV0YSIgY29tcG9uZW50LCBzb21lIG1ldGFkYXRhLg0KYGBge3J9DQojIENyZWF0ZSBjb3JwdXMNCmNvcnB1cyA9IENvcnB1cyhWZWN0b3JTb3VyY2UodHdlZXRzJHRleHQpKQ0KDQojIExvb2sgYXQgdGhlIGZpcnN0IHR3ZWV0J3MgdGV4dA0KY29ycHVzW1sxXV0kY29udGVudA0KYGBgDQoNCiMjIyAzLjIgQ29udmVydGluZyB0d2VldHMgdG8gbG93ZXJjYXNlDQoNCldlIHdhbnQgdG8gY29udmVydCBhbGwgdGhlIHRleHQgb2YgZWFjaCB0d2VldCB0byBsb3dlcmNhc2Ugc28gdGhlIGNsYXNzaWZpZXIgd29uJ3QgaGF2ZSB0byBkaXN0aW5ndWlzaCBiZXR3ZWVuICJKYXZhIiBhbmQgImphdmEiLCBmb3IgZXhhbXBsZS4NCmBgYHtyfQ0KIyBDb252ZXJ0IHRvIGxvd2VyLWNhc2UNCiMgdG1fbWFwKCkgYXBwbGllcyB0aGUgZnVuY3Rpb24gdG9sb3dlcigpIHRvIHRoZSBjb3JwdXMNCmNvcnB1cyA9IHRtX21hcChjb3JwdXMsIHRvbG93ZXIpDQoNCiMgaHR0cHM6Ly9kaXNjdXNzLmFuYWx5dGljc3ZpZGh5YS5jb20vdC9lcnJvci1pbmhlcml0cy1kb2MtdGV4dGRvY3VtZW50LWlzLW5vdC10cnVlLWluLXIvMTA3OC80DQpjb3JwdXMgPC0gdG1fbWFwKGNvcnB1cywgUGxhaW5UZXh0RG9jdW1lbnQpDQoNCiMgTG9vayBhdCB0aGUgZmlyc3QgdHdlZXQncyB0ZXh0DQpjb3JwdXNbWzFdXSRjb250ZW50DQpgYGANCg0KIyMjIDMuMyBSZW1vdmluZyBwdW5jdHVhdGlvbiBhbmQgc3RvcHdvcmRzDQoNClB1bmN0dWF0aW9uIGlzIG5vdCBoZWxwZnVsIHRvIGNsYXNzaWZ5IHRleHQsIHNvIHdlIGNhbiBkaXNjYXJkIGl0Lg0KYGBge3J9DQojIFJlbW92ZSBwdW5jdHVhdGlvbg0KIyB0bV9tYXAoKSBhcHBsaWVzIHRoZSBmdW5jdGlvbiByZW1vdmVQdW5jdHVhdGlvbigpIHRvIHRoZSBjb3JwdXMNCmNvcnB1cyA9IHRtX21hcChjb3JwdXMsIHJlbW92ZVB1bmN0dWF0aW9uKQ0KIyBMb29rIGF0IHRoZSBmaXJzdCB0d2VldCdzIHRleHQNCmNvcnB1c1tbMV1dJGNvbnRlbnQNCmBgYA0KDQpMaWtld2lzZSwgRW5nbGlzaCBzdG9wd29yZHMgc3VjaCBhcyBhcnRpY2xlcyAoInRoZSIsICJhIiwgImFuIiksIHByb25vdW5zICgid2UiLCAic2hlIiwgIml0IiwgLi4uKSwgdGhlIHZlcmIgInRvIGJlIiBhbmQgaXRzIGNvbmp1Z2F0aW9ucyAoImlzIiwgIndhcyIsICJ3ZXJlIiwgLi4uKSwgZXRjLiBkbyBub3QgYWlkIGluIGNsYXNzaWZpY2F0aW9uLiBXZSB3aWxsIGFsc28gcmVtb3ZlIHRoZSB0ZXJtICJqYXZhIiBmcm9tIGVhY2ggdHdlZXQsIHNpbmNlIGl0J3MgY29tbW9uIHRvIGFsbCB0d2VldHMgYW5kIHRoZXJlZm9yZSB3b24ndCBoZWxwIHVzIGVpdGhlci4NCmBgYHtyfQ0KIyBSZW1vdmUgRW5nbGlzaCBzdG9wd29yZHMNCiMgdG1fbWFwKCkgYXBwbGllcyB0aGUgZnVuY3Rpb24gcmVtb3ZlV29yZHMoKSBhbmQgaXRzIGFyZ3VtZW50cyB0byB0aGUgY29ycHVzDQpjb3JwdXMgPSB0bV9tYXAoY29ycHVzLCByZW1vdmVXb3JkcywgYygiamF2YSIsIHN0b3B3b3JkcygiZW5nbGlzaCIpKSkNCg0KIyBMb29rIGF0IHRoZSBmaXJzdCB0d2VldCdzIHRleHQNCmNvcnB1c1tbMV1dJGNvbnRlbnQNCmBgYA0KDQojIyMgMy40IFN0ZW1taW5nIHRoZSB0d2VldHMNCg0KVGhpcyBzdHJlYW1saW5lcyB0aGUgdHdlZXRzIGZ1cnRoZXIgYnkgcmVtb3Zpbmcgd29yZHMnIFtpbmZsZWN0aW9uc10oaHR0cDovL2VzbC5maXMuZWR1L2dyYW1tYXIvcnVsZXMvaW5mbGVjdGlvbnMuaHRtKS4gRS5nLiwgIkkgdHJpZWQgdG8uLi4iLCAiSSBoYXZlIGJlZW4gdHJ5aW5nIHRvLi4uIiwgIkkgd2lsbCB0cnkgdG8uLi4iIGFsbCBoYXZlIGNvbmp1Z2F0aW9ucyBvZiB0aGUgdmVyYiAidG8gdHJ5IiwgYnV0IHdlIGhhcmRseSBuZWVkIHRvIGluY2x1ZGUgYWxsIHRocmVlIG9mIHRoZW0gYXMgc2VwYXJhdGUgZmVhdHVyZXMuIEFub3RoZXIgZXhhbXBsZSB3b3VsZCBiZSB3b3JkIHBsdXJhbHMsIHN1Y2ggYXMgImpvYiIgYW5kICJqb2JzIi4NCmBgYHtyfQ0KIyBTdGVtIGRvY3VtZW50IA0KIyB0bV9tYXAoKSBhcHBsaWVzIHRoZSBmdW5jdGlvbiBzdGVtRG9jdW1lbnQoKSB0byB0aGUgY29ycHVzDQpjb3JwdXMgPSB0bV9tYXAoY29ycHVzLCBzdGVtRG9jdW1lbnQpDQoNCiMgTG9vayBhdCB0aGUgZmlyc3QgdHdlZXQncyB0ZXh0DQpjb3JwdXNbWzFdXSRjb250ZW50DQpgYGANCg0KIyMjIDMuNSBDcmVhdGluZyB0aGUgRG9jdW1lbnQtVGVybSBNYXRyaXgNCg0KVGhpcyB3aWxsIGNyZWF0ZSBhIG1hdHJpeCB3aGVyZSBlYWNoIHJvdyBpcyBhIGRvY3VtZW50ICh0d2VldCkgYW5kIGVhY2ggY29sdW1uIGlzIGEgdGVybSwgaS5lLiwgYSB3b3JkLiBFYWNoIGVudHJ5IGlzIGEgZnJlcXVlbmN5IG9mIGEgZ2l2ZW4gd29yZCBpbiBhIGdpdmVuIHR3ZWV0DQpgYGB7cn0NCiMgQ3JlYXRlIG1hdHJpeA0KZnJlcXVlbmNpZXMgPSBEb2N1bWVudFRlcm1NYXRyaXgoY29ycHVzKQ0KDQpmcmVxdWVuY2llcw0KYGBgDQoNCiMjIyAzLjYgRGVhbGluZyB3aXRoIERUTSBzcGFyc2l0eQ0KDQpUaGUgYGZyZXF1ZW5jaWVzYCBEVE0gaXMgKnZlcnkqIHNwYXJzZSwgd2hpY2ggbWVhbnMgYWxtb3N0IGFsbCBpdHMgZW50cmllcyBhcmUgMC4gV2UgaGF2ZSBhIG1hdHJpeCB3aXRoIGh1bmRyZWRzIG9mIHRob3VzYW5kcyBvZiBpdGVtcyBhbmQgb25seSBhIGZldyB0aG91c2FuZCBvZiB0aGVtIGFyZSBub24temVyby4gV2UgY2FuIHJlZHVjZSB0aGUgc2l6ZSBvZiB0aGUgbWF0cml4IGJ5IHJlcXVpcmluZyB0aGF0IGEgd29yZCAodGVybSkgYXBwZWFycyBpbiBhdCBsZWFzdCBhIGNlcnRhaW4gcGVyY2VudGFnZSBvZiB0aGUgdHdlZXRzIGZvciBpdCB0byBiZSBpbmNsdWRlZC4gSWYgaXQgZG9lc24ndCwgd2UgY2FuIGRpc2NhcmQgdGhhdCBjb2x1bW4NCmBgYHtyfQ0KIyBSZW1vdmUgc3BhcnNlIHRlcm1zDQojIFRoZSBzcGFyc2UgYXJndW1lbnQgaW4gcmVtb3ZlU3BhcnNlVGVybXMoKToNCiMgSWYgd2Ugc2V0IGl0IHRvIDAuOTksIHRoYXQgbWVhbnMgd2Ugd2FudCB0byBrZWVwIHRlcm1zIChjb2x1bW5zKSB0aGF0IGFwcGVhcg0KIyBpbiAxJSBvciBtb3JlIG9mIHRoZSBkb2N1bWVudHMgKHR3ZWV0cyBpbiB0aGlzIGNhc2UpLiBJZiB3ZSBzZXQgaXQgdG8gMC45OTUsDQojIHdlIHdhbnQgdG8ga2VlcCB0ZXJtcyB0aGF0IGFwcGVhciBpbiAwLjUlIG9yIG1vcmUgb2YgdGhlIHR3ZWV0cy4NCnNwYXJzZSA9IHJlbW92ZVNwYXJzZVRlcm1zKGZyZXF1ZW5jaWVzLCAwLjk5KQ0Kc3BhcnNlDQpgYGANCg0KV2UgaGF2ZSByZWR1Y2VkIHRoZSBzaXplIG9mIHRoZSBEVE0gYnkgb3ZlciBhbiBvcmRlciBvZiBtYWduaXR1ZGUsIGZyb20gMzYwMCB0ZXJtcyB0byA5OS4NCg0KIyMjIDMuNyBDb252ZXJ0aW5nIERUTSB0byBkYXRhZnJhbWUgYW5kIGZpbmFsIHRpZHlpbmcgdXANCg0KQ29udmVydCBgc3BhcnNlYCBmcm9tIERUTSB0byBhIGRhdGFmcmFtZQ0KYGBge3J9DQojIENvbnZlcnQgdG8gYSBkYXRhIGZyYW1lDQp0d2VldHNTcGFyc2UgPSBhcy5kYXRhLmZyYW1lKGFzLm1hdHJpeChzcGFyc2UpKQ0KY2xhc3ModHdlZXRzU3BhcnNlKQ0KYGBgDQoNCmBtYWtlLm5hbWVzKClgIG1ha2VzIGFsbCB2YXJpYWJsZSBuYW1lcyBSLWZyaWVuZGx5LiBUaGlzIGlzIGRvbmUgc3BlY2lhbGx5IGZvciBudW1iZXJzLCB3aGljaCBjYW4ndCBiZSB1c2VkIGFzIHZhcmlhYmxlcy4gU28gZm9yIGV4YW1wbGUsIGBtYWtlLm5hbWVzKCI1IilgIHJldHVybnMgIlg1Ii4NCmBgYHtyfQ0KIyBNYWtlIGFsbCB0ZXJtcyBSLWZyaWVuZGx5DQpjb2xuYW1lcyh0d2VldHNTcGFyc2UpID0gbWFrZS5uYW1lcyhjb2xuYW1lcyh0d2VldHNTcGFyc2UpKQ0KYGBgDQoNCkNvbnZlcnQgdGhlIGNsYXNzaWZpY2F0aW9uIGNvbHVtbiAkY2xhc3MkIGZyb20gY2hhcmFjdGVyIHRvIGZhY3Rvci4NCmBgYHtyfQ0KIyBDb252ZXJ0aW5nIGZyb20gY2hhcmFjdGVyIHRvIGZhY3Rvcg0KdHdlZXRzU3BhcnNlJGNsYXNzID0gYXMuZmFjdG9yKHR3ZWV0cyRjbGFzcykNCmBgYA0KDQojIyA0LjAgRGF0YSB2aXN1YWxpemF0aW9uOiB3b3JkIGNsb3Vkcw0KDQpCZWZvcmUgYnVpbGRpbmcgdGhlIFNWTSBjbGFzc2lmaWVyLCB3ZSBjYW4gdmlzdWFsaXplIHRoZSB0ZXJtICh3b3JkIHN0ZW0pIGZyZXF1ZW5jaWVzIG9mIHRoZSB0d2VldHMnIGNsYXNzZXMuDQoNCmBgYHtyLCBldmFsPUZBTFNFLCBmaWcuaGVpZ2h0PTE0LCBmaWcud2lkdGg9OCwgaW5jbHVkZT1GQUxTRX0NCmxheW91dChtYXRyaXgoYyhyZXAoMSwzKSwgcmVwKDIsMyksIHJlcCgzLDMpLCByZXAoNCwzKSwgcmVwKDUsMyksIHJlcCg2LDMpLCByZXAoNywzKSwgcmVwKDgsMyksICByZXAoOSwyKSwgcmVwKDEwLDIpLCByZXAoMTEsMiksIHJlcCgxMiwyKSwgcmVwKDEzLDIpLCByZXAoMTQsMikgKSwgbmNvbCA9IDYsIGJ5cm93PVRSVUUpLCBoZWlnaHRzID0gYygxLDYsMSw2LDEsNikpDQoNCnBhcihtYXI9cmVwKDAsIDQpKQ0KDQpwbG90Lm5ldygpDQp0ZXh0KHg9MC41LCB5PTAuNSwgIkpvYnMgd29yZCBjbG91ZCIsIGNleCA9IDEuNSwgZm9udCA9IDIpDQpwbG90Lm5ldygpDQp0ZXh0KHg9MC41LCB5PTAuNSwgIkxlYXJuaW5nIHdvcmQgY2xvdWQiLCBjZXggPSAxLjUsIGZvbnQgPSAyKQ0KDQp3b3JkY2xvdWQod29yZHMgPSBkJHdvcmQsIGZyZXEgPSBkJGZyZXEsIG1pbi5mcmVxID0gMiwgDQogICAgICAgICAgbWF4LndvcmRzPTIwMCwgcmFuZG9tLm9yZGVyPUZBTFNFLCByb3QucGVyPTAuMzUsIA0KICAgICAgICAgIGNvbG9ycz1icmV3ZXIucGFsKDgsICJEYXJrMiIpLCBzY2FsZSA9IGMoOSwgMC45KSkNCg0Kd29yZGNsb3VkKHdvcmRzID0gZnJlcV9kZl9sZWFybmluZyR3b3JkLCBmcmVxID0gZnJlcV9kZl9sZWFybmluZyRmcmVxLCBtaW4uZnJlcSA9IDEsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSwgc2NhbGUgPSBjKDQuNSwgMC40NSkpDQoNCnBsb3QubmV3KCkNCnRleHQoeD0wLjUsIHk9MC41LCAiT3BpbmlvbiB3b3JkIGNsb3VkIiwgY2V4ID0gMS4zLCBmb250ID0gMikNCnBsb3QubmV3KCkNCnRleHQoeD0wLjUsIHk9MC41LCAiSW5kb25lc2lhIHdvcmQgY2xvdWQiLCBjZXggPSAxLjMsIGZvbnQgPSAyKQ0KDQp3b3JkY2xvdWQod29yZHMgPSBmcmVxX2RmX29waW5pb24kd29yZCwgZnJlcSA9IGZyZXFfZGZfb3BpbmlvbiRmcmVxLCBtaW4uZnJlcSA9IDEsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSwgc2NhbGUgPSBjKDUsIDAuNSkpDQoNCndvcmRjbG91ZCh3b3JkcyA9IGZyZXFfZGZfSW5kb25lc2lhJHdvcmQsIGZyZXEgPSBmcmVxX2RmX0luZG9uZXNpYSRmcmVxLCBtaW4uZnJlcSA9IDEsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSwgc2NhbGUgPSBjKDgsIDAuOCkpDQoNCnBsb3QubmV3KCkNCnRleHQoeD0wLjUsIHk9MC41LCAiVGl0bGUgb2YgbXkgZmlmdGggcGxvdCIpDQpwbG90Lm5ldygpDQp0ZXh0KHg9MC41LCB5PTAuNSwgIlRpdGxlIG9mIG15IHNpeHRoIHBsb3QiKQ0KcGxvdC5uZXcoKQ0KdGV4dCh4PTAuNSwgeT0wLjUsICJUaXRsZSBvZiBteSBzZXZlbnRoIHBsb3QiKQ0KDQp3b3JkY2xvdWQod29yZHMgPSBkJHdvcmQsIGZyZXEgPSBkJGZyZXEsIG1pbi5mcmVxID0gMiwgDQogICAgICAgICAgbWF4LndvcmRzPTIwMCwgcmFuZG9tLm9yZGVyPUZBTFNFLCByb3QucGVyPTAuMzUsIA0KICAgICAgICAgIGNvbG9ycz1icmV3ZXIucGFsKDgsICJEYXJrMiIpLCBzY2FsZSA9IGMoNSwgMC41KSkNCg0Kd29yZGNsb3VkKHdvcmRzID0gZCR3b3JkLCBmcmVxID0gZCRmcmVxLCBtaW4uZnJlcSA9IDIsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSkNCg0Kd29yZGNsb3VkKHdvcmRzID0gZCR3b3JkLCBmcmVxID0gZCRmcmVxLCBtaW4uZnJlcSA9IDIsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSkNCmBgYA0KDQpgYGB7ciwgZmlnLndpZHRoID0gOCwgZmlnLmhlaWdodCA9IDEyfQ0KIyBodHRwOi8vd3d3LnN0aGRhLmNvbS9lbmdsaXNoL3dpa2kvdGV4dC1taW5pbmctYW5kLXdvcmQtY2xvdWQtZnVuZGFtZW50YWxzLWluLXItNS1zaW1wbGUtc3RlcHMteW91LXNob3VsZC1rbm93DQpsYXlvdXQobWF0cml4KGMocmVwKDEsMyksIHJlcCgyLDMpLCByZXAoMywzKSwgcmVwKDQsMyksIHJlcCg1LDIpLCByZXAoNiwyKSwgcmVwKDcsMikpLCBuY29sID0gNiwgYnlyb3c9VFJVRSkpDQojbGF5b3V0LnNob3coNCkNCnBhcihtYXI9cmVwKDAsIDQpKQ0Kc2V0LnNlZWQoNTY3OCkNCg0KbGlicmFyeSh3b3JkY2xvdWQpDQoNCiMgSm9iIHR3ZWV0cycgd29yZCBjbG91ZA0KdHdlZXRzU3BhcnNlSm9icyA8LSB0d2VldHNTcGFyc2VbdHdlZXRzU3BhcnNlJGNsYXNzID09ICJqb2IiLF0NCnR3ZWV0c1NwYXJzZUpvYnNNYXRyaXggPC0gYXMubWF0cml4KHR3ZWV0c1NwYXJzZUpvYnNbLCAhbmFtZXModHdlZXRzU3BhcnNlSm9icykgJWluJSBjKCJjbGFzcyIpXSkNCmZyZXFfdGFibGVfam9icyA8LSBzb3J0KGNvbFN1bXModHdlZXRzU3BhcnNlSm9ic01hdHJpeCksZGVjcmVhc2luZz1UUlVFKQ0KZnJlcV9kZl9qb2JzIDwtIGRhdGEuZnJhbWUod29yZCA9IG5hbWVzKGZyZXFfdGFibGVfam9icyksIGZyZXE9ZnJlcV90YWJsZV9qb2JzKQ0KDQp3b3JkY2xvdWQod29yZHMgPSBmcmVxX2RmX2pvYnMkd29yZCwgZnJlcSA9IGZyZXFfZGZfam9icyRmcmVxLCBtaW4uZnJlcSA9IDEsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSwgc2NhbGUgPSBjKDcsIDAuNykpDQp0ZXh0KHg9MC41LCB5PTEsICJKb2JzIHdvcmQgY2xvdWQiLCBjZXggPSAxLjUsIGZvbnQgPSAyKQ0KDQojIExlYXJuaW5nIHR3ZWV0cycgd29yZCBjbG91ZA0KdHdlZXRzU3BhcnNlTGVhcm5pbmcgPC0gdHdlZXRzU3BhcnNlW3R3ZWV0c1NwYXJzZSRjbGFzcyA9PSAibGVhcm5pbmciLF0NCnR3ZWV0c1NwYXJzZUxlYXJuaW5nTWF0cml4IDwtIGFzLm1hdHJpeCh0d2VldHNTcGFyc2VMZWFybmluZ1ssICFuYW1lcyh0d2VldHNTcGFyc2VMZWFybmluZykgJWluJSBjKCJjbGFzcyIpXSkNCmZyZXFfdGFibGVfbGVhcm5pbmcgPC0gc29ydChjb2xTdW1zKHR3ZWV0c1NwYXJzZUxlYXJuaW5nTWF0cml4KSxkZWNyZWFzaW5nPVRSVUUpDQpmcmVxX2RmX2xlYXJuaW5nIDwtIGRhdGEuZnJhbWUod29yZCA9IG5hbWVzKGZyZXFfdGFibGVfbGVhcm5pbmcpLCBmcmVxPWZyZXFfdGFibGVfbGVhcm5pbmcpDQoNCndvcmRjbG91ZCh3b3JkcyA9IGZyZXFfZGZfbGVhcm5pbmckd29yZCwgZnJlcSA9IGZyZXFfZGZfbGVhcm5pbmckZnJlcSwgbWluLmZyZXEgPSAxLCANCiAgICAgICAgICBtYXgud29yZHM9MjAwLCByYW5kb20ub3JkZXI9RkFMU0UsIHJvdC5wZXI9MC4zNSwgDQogICAgICAgICAgY29sb3JzPWJyZXdlci5wYWwoOCwgIkRhcmsyIiksIHNjYWxlID0gYyg1LCAwLjUpKQ0KdGV4dCh4PTAuNSwgeT0xLCAiTGVhcm5pbmcgd29yZCBjbG91ZCIsIGNleCA9IDEuNSwgZm9udCA9IDIpDQoNCiMgT3BpbmlvbiB0d2VldHMnIHdvcmQgY2xvdWQNCnR3ZWV0c1NwYXJzZU9waW5pb24gPC0gdHdlZXRzU3BhcnNlW3R3ZWV0c1NwYXJzZSRjbGFzcyA9PSAib3BpbmlvbiIsXQ0KdHdlZXRzU3BhcnNlT3Bpbmlvbk1hdHJpeCA8LSBhcy5tYXRyaXgodHdlZXRzU3BhcnNlT3BpbmlvblssICFuYW1lcyh0d2VldHNTcGFyc2VPcGluaW9uKSAlaW4lIGMoImNsYXNzIildKQ0KZnJlcV90YWJsZV9vcGluaW9uIDwtIHNvcnQoY29sU3Vtcyh0d2VldHNTcGFyc2VPcGluaW9uTWF0cml4KSxkZWNyZWFzaW5nPVRSVUUpDQpmcmVxX2RmX29waW5pb24gPC0gZGF0YS5mcmFtZSh3b3JkID0gbmFtZXMoZnJlcV90YWJsZV9vcGluaW9uKSwgZnJlcT1mcmVxX3RhYmxlX29waW5pb24pDQoNCndvcmRjbG91ZCh3b3JkcyA9IGZyZXFfZGZfb3BpbmlvbiR3b3JkLCBmcmVxID0gZnJlcV9kZl9vcGluaW9uJGZyZXEsIG1pbi5mcmVxID0gMSwgDQogICAgICAgICAgbWF4LndvcmRzPTIwMCwgcmFuZG9tLm9yZGVyPUZBTFNFLCByb3QucGVyPTAuMzUsIA0KICAgICAgICAgIGNvbG9ycz1icmV3ZXIucGFsKDgsICJEYXJrMiIpLCBzY2FsZSA9IGMoNC4wLCAwLjQpKQ0KdGV4dCh4PTAuNSwgeT0xLCAiT3BpbmlvbiB3b3JkIGNsb3VkIiwgY2V4ID0gMS41LCBmb250ID0gMikNCg0KIyBJbmRvbmVzaWEgdHdlZXRzJyB3b3JkIGNsb3VkDQp0d2VldHNTcGFyc2VJbmRvbmVzaWEgPC0gdHdlZXRzU3BhcnNlW3R3ZWV0c1NwYXJzZSRjbGFzcyA9PSAiSW5kb25lc2lhIixdDQp0d2VldHNTcGFyc2VJbmRvbmVzaWFNYXRyaXggPC0gYXMubWF0cml4KHR3ZWV0c1NwYXJzZUluZG9uZXNpYVssICFuYW1lcyh0d2VldHNTcGFyc2VJbmRvbmVzaWEpICVpbiUgYygiY2xhc3MiKV0pDQpmcmVxX3RhYmxlX0luZG9uZXNpYSA8LSBzb3J0KGNvbFN1bXModHdlZXRzU3BhcnNlSW5kb25lc2lhTWF0cml4KSxkZWNyZWFzaW5nPVRSVUUpDQpmcmVxX2RmX0luZG9uZXNpYSA8LSBkYXRhLmZyYW1lKHdvcmQgPSBuYW1lcyhmcmVxX3RhYmxlX0luZG9uZXNpYSksIGZyZXE9ZnJlcV90YWJsZV9JbmRvbmVzaWEpDQoNCndvcmRjbG91ZCh3b3JkcyA9IGZyZXFfZGZfSW5kb25lc2lhJHdvcmQsIGZyZXEgPSBmcmVxX2RmX0luZG9uZXNpYSRmcmVxLCBtaW4uZnJlcSA9IDEsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSwgc2NhbGUgPSBjKDgsIDAuOCkpDQp0ZXh0KHg9MC41LCB5PTEsICJJbmRvbmVzaWEgd29yZCBjbG91ZCIsIGNleCA9IDEuNSwgZm9udCA9IDIpDQoNCiMgQ29mZmVlIHR3ZWV0cycgd29yZCBjbG91ZA0KdHdlZXRzU3BhcnNlQ29mZmVlIDwtIHR3ZWV0c1NwYXJzZVt0d2VldHNTcGFyc2UkY2xhc3MgPT0gImNvZmZlZSIsXQ0KdHdlZXRzU3BhcnNlQ29mZmVlTWF0cml4IDwtIGFzLm1hdHJpeCh0d2VldHNTcGFyc2VDb2ZmZWVbLCAhbmFtZXModHdlZXRzU3BhcnNlQ29mZmVlKSAlaW4lIGMoImNsYXNzIildKQ0KZnJlcV90YWJsZV9jb2ZmZWUgPC0gc29ydChjb2xTdW1zKHR3ZWV0c1NwYXJzZUNvZmZlZU1hdHJpeCksZGVjcmVhc2luZz1UUlVFKQ0KZnJlcV9kZl9jb2ZmZWUgPC0gZGF0YS5mcmFtZSh3b3JkID0gbmFtZXMoZnJlcV90YWJsZV9jb2ZmZWUpLCBmcmVxPWZyZXFfdGFibGVfY29mZmVlKQ0KDQp3b3JkY2xvdWQod29yZHMgPSBmcmVxX2RmX2NvZmZlZSR3b3JkLCBmcmVxID0gZnJlcV9kZl9jb2ZmZWUkZnJlcSwgbWluLmZyZXEgPSAxLCANCiAgICAgICAgICBtYXgud29yZHM9MjAwLCByYW5kb20ub3JkZXI9RkFMU0UsIHJvdC5wZXI9MC4zNSwgDQogICAgICAgICAgY29sb3JzPWJyZXdlci5wYWwoOCwgIkRhcmsyIiksIHNjYWxlID0gYyg2LCAwLjYpKQ0KdGV4dCh4PTAuNSwgeT0xLCAiQ29mZmVlIHdvcmQgY2xvdWQiLCBjZXggPSAxLjUsIGZvbnQgPSAyKQ0KDQojIEhlbHAgdHdlZXRzJyB3b3JkIGNsb3VkDQp0d2VldHNTcGFyc2VIZWxwIDwtIHR3ZWV0c1NwYXJzZVt0d2VldHNTcGFyc2UkY2xhc3MgPT0gImhlbHAiLF0NCnR3ZWV0c1NwYXJzZUhlbHBNYXRyaXggPC0gYXMubWF0cml4KHR3ZWV0c1NwYXJzZUhlbHBbLCAhbmFtZXModHdlZXRzU3BhcnNlSGVscCkgJWluJSBjKCJjbGFzcyIpXSkNCmZyZXFfdGFibGVfaGVscCA8LSBzb3J0KGNvbFN1bXModHdlZXRzU3BhcnNlSGVscE1hdHJpeCksZGVjcmVhc2luZz1UUlVFKQ0KZnJlcV9kZl9oZWxwIDwtIGRhdGEuZnJhbWUod29yZCA9IG5hbWVzKGZyZXFfdGFibGVfaGVscCksIGZyZXE9ZnJlcV90YWJsZV9oZWxwKQ0KDQp3b3JkY2xvdWQod29yZHMgPSBmcmVxX2RmX2hlbHAkd29yZCwgZnJlcSA9IGZyZXFfZGZfaGVscCRmcmVxLCBtaW4uZnJlcSA9IDEsIA0KICAgICAgICAgIG1heC53b3Jkcz0yMDAsIHJhbmRvbS5vcmRlcj1GQUxTRSwgcm90LnBlcj0wLjM1LCANCiAgICAgICAgICBjb2xvcnM9YnJld2VyLnBhbCg4LCAiRGFyazIiKSwgc2NhbGUgPSBjKDUsIDAuNSkpDQp0ZXh0KHg9MC41LCB5PTEsICJIZWxwIHdvcmQgY2xvdWQiLCBjZXggPSAxLjUsIGZvbnQgPSAyKQ0KDQojIElycmVsZXZhbnQgdHdlZXRzJyB3b3JkIGNsb3VkDQp0d2VldHNTcGFyc2VJcnJlbCA8LSB0d2VldHNTcGFyc2VbdHdlZXRzU3BhcnNlJGNsYXNzID09ICJpcnJlbGV2YW50IixdDQp0d2VldHNTcGFyc2VJcnJlbE1hdHJpeCA8LSBhcy5tYXRyaXgodHdlZXRzU3BhcnNlSXJyZWxbLCAhbmFtZXModHdlZXRzU3BhcnNlSXJyZWwpICVpbiUgYygiY2xhc3MiKV0pDQpmcmVxX3RhYmxlX2lycmVsIDwtIHNvcnQoY29sU3Vtcyh0d2VldHNTcGFyc2VJcnJlbE1hdHJpeCksZGVjcmVhc2luZz1UUlVFKQ0KZnJlcV9kZl9pcnJlbCA8LSBkYXRhLmZyYW1lKHdvcmQgPSBuYW1lcyhmcmVxX3RhYmxlX2lycmVsKSwgZnJlcT1mcmVxX3RhYmxlX2lycmVsKQ0KDQp3b3JkY2xvdWQod29yZHMgPSBmcmVxX2RmX2lycmVsJHdvcmQsIGZyZXEgPSBmcmVxX2RmX2lycmVsJGZyZXEsIG1pbi5mcmVxID0gMSwgDQogICAgICAgICAgbWF4LndvcmRzPTIwMCwgcmFuZG9tLm9yZGVyPUZBTFNFLCByb3QucGVyPTAuMzUsIA0KICAgICAgICAgIGNvbG9ycz1icmV3ZXIucGFsKDgsICJEYXJrMiIpLCBzY2FsZSA9IGMoNSwgMC41KSkNCnRleHQoeD0wLjUsIHk9MSwgIklycmVsZXZhbnQgd29yZCBjbG91ZCIsIGNleCA9IDEuNSwgZm9udCA9IDIpDQpgYGANCg0KVGhlcmUgYXJlIHNldmVyYWwgdGVybXMgdGhhdCBhcHBlYXIgZnJlcXVlbnRseSBpbiBtb3JlIHRoYW4gb25lIGNhdGVnb3J5LiBGb3IgZXhhbXBsZSwgImRldmVsb3AiIGFwcGVhcnMgb2Z0ZW4gaW4gdHdlZXRzIHJlbGF0ZWQgdG8gYm90aCBqb2JzIGFuZCBsZWFybmluZyBvcHBvcnR1bml0aWVzLiBMaWtld2lzZSwgInB5dGhvbiIgYW5kICJqYXZhc2NyaXB0IiBjb21tb25seSBzaG93IHVwIGluIGJvdGggb3BpbmlvbiBhbmQgbGVhcm5pbmcgdHdlZXRzLg0KDQojIyA1LjAgQnVpbGRpbmcgdGhlIFNWTSBjbGFzc2lmaWVyDQoNCk5vdyB3ZSBhcmUgcmVhZHkgdG8gYnVpbGQgdGhlIGNsYXNzaWZpZXIuDQoNCiMjIyA1LjEgU3BsaXR0aW5nIGludG8gdHJhaW5pbmcgYW5kIHRlc3Rpbmcgc2V0cw0KDQpXZSB3aWxsIHNwbGl0IHRoZSBkYXRhIDcwLzMwIGludG8gdHJhaW5pbmcgYW5kIHRlc3Rpbmcgc2V0cw0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQojIFNwbGl0IHRoZSBkYXRhDQoNCmxpYnJhcnkoY2FUb29scykNCg0Kc2V0LnNlZWQoNTY3OCkNCg0Kc3BsaXQgPSBzYW1wbGUuc3BsaXQodHdlZXRzU3BhcnNlJGNsYXNzLCBTcGxpdFJhdGlvID0gMC43KQ0KDQp0cmFpblNwYXJzZSA9IHN1YnNldCh0d2VldHNTcGFyc2UsIHNwbGl0PT1UUlVFKQ0KdGVzdFNwYXJzZSA9IHN1YnNldCh0d2VldHNTcGFyc2UsIHNwbGl0PT1GQUxTRSkNCmNhdCgiRGltZW5zaW9ucyBvZiB0cmFpbmluZyBkYXRhc2V0OiAiLCBkaW0odHJhaW5TcGFyc2UpLCAiXG5EaW1lbnNpb25zIG9mIHRlc3QgZGF0YXNldDogIiwgZGltKHRlc3RTcGFyc2UpKQ0KYGBgDQoNCiMjIyA1LjIgQW4gU1ZNIGNsYXNzaWZpZXIgd2l0aCBhbiBSQkYga2VybmVsDQoNCldlIHdpbGwgdHJhaW4gdGhlIFsqKmUxMDcxKipdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy90bS90bS5wZGYpIGxpYnJhcnkncyBpbXBsZW1lbnRhdGlvbiBvZiBbU1ZNXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvZTEwNzEvdmlnbmV0dGVzL3N2bWRvYy5wZGYpIHVzaW5nIGEgUmFkaWFsIEJhc2lzIEZ1bmN0aW9uIFtrZXJuZWxdKGh0dHBzOi8vYmxvZy5zdGF0c2JvdC5jby9zdXBwb3J0LXZlY3Rvci1tYWNoaW5lcy10dXRvcmlhbC1jMTYxOGU2MzVlOTMpLiBQbGVhc2Ugc2VlIFNlY3Rpb24gOS4zLjIgb2YgW0lTTFJdKGh0dHA6Ly93d3ctYmNmLnVzYy5lZHUvfmdhcmV0aC9JU0wvSVNMUiUyMEZpcnN0JTIwUHJpbnRpbmcucGRmKSBmb3IgYW4gb3ZlcnZpZXcgb2YgdGhlIFJCRiBrZXJuZWwuIFdlIHdpbGwgZm9sbG93IHRoZSBbYWR2aWNlXShodHRwczovL3d3dy5jc2llLm50dS5lZHUudHcvfmNqbGluL3BhcGVycy9ndWlkZS9ndWlkZS5wZGYpIG9mIHRoZSBmb2xrcyBiZWhpbmQgdGhlIExJQlNWTSBhbGdvcml0aG0gdGhhdCBpcyBpbXBsZW1lbnRlZCBieSB0aGUgKiplMTA3MSoqIGxpYnJhcnkgYW5kIHRyeSB0aGUgUkJGIGtlcm5lbCBmaXJzdC4NCg0KIyMjIyA1LjIuMSBBbiBTVk0gd2l0aCBSQkYga2VybmVsIGV4YW1wbGUNCg0KTW9zdCBvZiB0aGUgZXhhbXBsZXMgb2Ygc3VwcG9ydCB2ZWN0b3IgY2xhc3NpZmljYXRpb24sIHdoZXRoZXIgdXNpbmcgbGluZWFyLCBwb2x5bm9taWFsLCBvciBSQkYga2VybmVscywgaW52b2x2ZSB0d28gY2xhc3Nlcy4gSSB3YW50ZWQgdG8gdmlzdWFsaXplIGhvdyBpdCB3b3JrZWQgaW4gYSBjYXNlIHdoZXJlIHRoZSBkYXRhIGhhZCBtb3JlIHRoYW4gdHdvIGNsYXNzZXMsIHNpbmNlIGluIG91ciBjYXNlIHdlIGhhdmUgc2V2ZW4uIEkgY3JlYXRlZCBhbiBhcnRpZmljaWFsIGZvdXItY2xhc3MgZGF0YSBzYW1wbGUgd2hlcmUgdGhlIGRhdGEgaXMgZGVmaW5pdGVseSBub3QgbGluZWFybHkgc2VwYXJhYmxlLCBhbmQgdHJpZWQgdG8gdHJhaW4gdGhlIFNWTSB3aXRoIFJCRiBrZXJuZWwgb24gaXQuIEhlcmUgaXMgdGhlIG1hZGUtdXAgZXhhbXBsZSBkYXRhOg0KDQpgYGB7ciwgZmlnLndpZHRoID0gNSwgZmlnLmFzcCA9IDF9DQpzZXQuc2VlZCg1Njc4KQ0KeDQxIDwtIHJ1bmlmKDIwMCwgbWluID0gLTMsIG1heCA9IDMpDQp4NDIgPC0gcnVuaWYoMjAwLCBtaW4gPSAtMywgbWF4ID0gMykNCnk0MSA8LSBpZmVsc2Uoc3FydCh4NDFeMiArIHg0Ml4yKSA8IDEuMCwgMSwgaWZlbHNlKHNxcnQoeDQxXjIgKyB4NDJeMikgPiAxLjAgJiBzcXJ0KHg0MV4yICsgeDQyXjIpIDwgMi4wLCAyLCBpZmVsc2Uoc3FydCh4NDFeMiArIHg0Ml4yKSA+IDIuMCAmIHNxcnQoeDQxXjIgKyB4NDJeMikgPCAzLjAsIDMsIDQpKSkNCnBsb3QoeDQxLCB4NDIsIGNvbCA9IGlmZWxzZShzcXJ0KHg0MV4yICsgeDQyXjIpIDwgMS4wLCAicmVkIiwgaWZlbHNlKHNxcnQoeDQxXjIgKyB4NDJeMikgPiAxLjAgJiBzcXJ0KHg0MV4yICsgeDQyXjIpIDwgMi4wLCAiZ3JlZW4iLCBpZmVsc2Uoc3FydCh4NDFeMiArIHg0Ml4yKSA+IDIuMCAmIHNxcnQoeDQxXjIgKyB4NDJeMikgPCAzLjAsICJibHVlIiwgImdyYXkiKSkpLCBwY2ggPSAxOSkNCg0KIyBQbG90dGluZyBjaXJjbGVzDQojIGh0dHBzOi8vc3RhY2tvdmVyZmxvdy5jb20vcXVlc3Rpb25zLzIyMjY1NzA0L2RyYXdpbmctY2lyY2xlLWluLXINCnJhZGl1cyA8LSBjKDE6MykNCnRoZXRhIDwtIHNlcSgwLCAyICogcGksIGxlbmd0aCA9IDIwMCkNCg0KIyBkcmF3IHRoZSBjaXJjbGVzDQpsaW5lcyh4ID0gcmFkaXVzWzFdICogY29zKHRoZXRhKSwgeSA9IHJhZGl1c1sxXSAqIHNpbih0aGV0YSkpDQpsaW5lcyh4ID0gcmFkaXVzWzJdICogY29zKHRoZXRhKSwgeSA9IHJhZGl1c1syXSAqIHNpbih0aGV0YSkpDQpsaW5lcyh4ID0gcmFkaXVzWzNdICogY29zKHRoZXRhKSwgeSA9IHJhZGl1c1szXSAqIHNpbih0aGV0YSkpDQpgYGANCg0KVGhlIGRhdGEgaGFzIGJlZW4gYXJiaXRyYXJpbHkgY2xhc3NpZmllZCBpbnNpZGUgb3Igb3V0c2lkZSBlaXRoZXIgcmluZ3Mgb3IgY2lyY2xlcy4gV2Ugb25seSBoYXZlIDIgcHJlZGljdG9ycyBzbyB3ZSBjYW4gdmlzdWFsaXplIGl0LiBIZXJlIGlzIHRoZSBTVk0tUkJGIGltcGxlbWVudGF0aW9uLiBXZSB3aWxsIGZvbGxvdyB0aGUgW2FkdmljZV0oaHR0cHM6Ly93d3cuY3NpZS5udHUuZWR1LnR3L35jamxpbi9wYXBlcnMvZ3VpZGUvZ3VpZGUucGRmKSBvZiB0aGUgZm9sa3MgYmVoaW5kIHRoZSBMSUJTVk0gYWxnb3JpdGhtIHRoYXQgaXMgaW1wbGVtZW50ZWQgYnkgdGhlICoqZTEwNzEqKiBsaWJyYXJ5IGFuZCB1c2UgY3Jvc3MtdmFsaWRhdGlvbiB0byBmaW5kIHRoZSBiZXN0IHBhcmFtZXRlcnMuIFdlIGNhbiB0dW5lIHRoZSBtb2RlbCB1c2luZyBgdHVuZS5zdm0oKWAsIHdoaWNoIGZpbmRzIG9wdGltYWwgdmFsdWVzIG9mICRcZ2FtbWEkIGFuZCAkQyQgdXNpbmcgMTAtZm9sZCBjcm9zcy12YWxpZGF0aW9uLiBIZXJlIHdlIGFyZSB1c2luZyB2YWx1ZXMgb2YgJFxnYW1tYSQgZnJvbSAkMTBeey0zfSQgdG8gJDEwXnstMX0kLCB3aXRoICRDJCB2YWx1ZXMgb2YgJDEkLCAkMTAkLCBhbmQgJDEwMCQuDQpgYGB7ciwgZmlnLndpZHRoID0gNiwgZmlnLmFzcCA9IDAuOH0NCnk0MSA8LSBhcy5mYWN0b3IoeTQxKQ0KbXlfZGY0IDwtIGRhdGEuZnJhbWUoeDQyLCB4NDEsIHk0MSkNCnNldC5zZWVkKDU2NzgpDQpsaWJyYXJ5KGUxMDcxKQ0KbXlfbW9kZWw0X3R1bmVkIDwtIHR1bmUuc3ZtKHk0MX4uLCBkYXRhID0gbXlfZGY0LCBrZXJuZWwgPSAicmFkaWFsIiwgZ2FtbWEgPSAxMF4oLTM6LTEpLCBjb3N0ID0gMTBeKDA6MikpDQpnYW1tYV9iZXN0IDwtIG15X21vZGVsNF90dW5lZCRiZXN0LnBhcmFtZXRlcnMkZ2FtbWENCmNvc3RfYmVzdCA8LSBteV9tb2RlbDRfdHVuZWQkYmVzdC5wYXJhbWV0ZXJzJGNvc3QNCm15X21vZGVsNCA8LSBzdm0oeTQxIH4gLiwgZGF0YSA9IG15X2RmNCwga2VybmVsID0gInJhZGlhbCIsIGdhbW1hID0gZ2FtbWFfYmVzdCwgY29zdCA9IGNvc3RfYmVzdCkNCnBsb3QobXlfbW9kZWw0LCBteV9kZjQsIHhsaW0gPSBjKC0zLDMpLCB5bGltID0gYygtMywzKSkNCmBgYA0KDQojIyMjIDUuMi4yIFRyYWluaW5nIHRoZSBTVk0gd2l0aCBSQkYga2VybmVsIG9uIHRoZSB0d2l0dGVyIGRhdGENCg0KV2UgY2FuIGZvbGxvdyB0aGUgc2FtZSBwcm9jZWR1cmUgdG8gdHJhaW4gdGhlIFNWTSBvbiB0aGUgdHdpdHRlciBkYXRhLiBIZXJlIHdlIHdpbGwgbm90IGJlIGFibGUgdG8gdmlzdWFsaXplIHRoZSBkYXRhIHNpbmNlIHRoZSB0aGVyZSBhcmUgbWFueSBwcmVkaWN0b3JzLg0KYGBge3IsIHdhcm5pbmc9RkFMU0V9DQpzZXQuc2VlZCg1Njc4KQ0KdHVuZWQgPC0gdHVuZS5zdm0oY2xhc3N+LiwgZGF0YSA9IHRyYWluU3BhcnNlLCBnYW1tYSA9IDEwXigtNjotMSksIGNvc3QgPSAxMF4oMDoyKSkNCmdhbW1hX2Jlc3QgPC0gdHVuZWQkYmVzdC5wYXJhbWV0ZXJzJGdhbW1hDQpjb3N0X2Jlc3QgPC0gdHVuZWQkYmVzdC5wYXJhbWV0ZXJzJGNvc3QNCm1vZGVsLnR1bmVkIDwtIHN2bShjbGFzc34uLCBkYXRhID0gdHJhaW5TcGFyc2UsIGdhbW1hID0gZ2FtbWFfYmVzdCwgY29zdCA9IGNvc3RfYmVzdCkNCmBgYA0KDQpMZXQncyBzZWUgaG93IHRoZSB0dW5lZCBTVk0gbW9kZWwgcGVyZm9ybXMgb24gdGhlIHRlc3QgZGF0YS4NCmBgYHtyfQ0Kc3ZtLnR1bmVkLnByZWQgPC0gcHJlZGljdChtb2RlbC50dW5lZCwgbmV3ZGF0YT10ZXN0U3BhcnNlLCB0eXBlPSJjbGFzcyIpDQpzdm0udHVuZWQudGFibGUgPC0gdGFibGUodGVzdFNwYXJzZSRjbGFzcywgc3ZtLnR1bmVkLnByZWQpDQpzdm0udHVuZWQudGFibGUNCmBgYA0KDQpQZXJmb3JtYW5jZSBvZiB0aGUgdHVuZWQgbW9kZWwgb24gdGhlIHRlc3Qgc2V0DQpgYGB7cn0NCmFjY3VyYWN5X3R1bmVkX3N2bSA8LSBzdW0oZGlhZyhzdm0udHVuZWQudGFibGUpKSAvIHN1bShzdm0udHVuZWQudGFibGUpDQpjYXQoIkFjY3VyYWN5IG9mIHR1bmVkIFNWTToiLCBhY2N1cmFjeV90dW5lZF9zdm0pDQpgYGANCg0KVGhlIGFjY3VyYWN5IG9mIHRoZSBTVk0gbW9kZWwgaXMgZ29vZC4gSXQgZG9lcyB3ZWxsIGZvciB0aGUgdHdvIG1vc3QgZnJlcXVlbnQgdHdlZXQgY2F0ZWdvcmllcywgam9icyBhbmQgbGVhcm5pbmcsIGJ1dCBpdCB0ZW5kcyB0byBtaXNjbGFzc2lmeSB0aGUgb3BpbmlvbiB0d2VldHMgYXMgbGVhcm5pbmcgb25lcy4gSXQgY29ycmVjdGx5IGNsYXNzaWZpZWQgb25seSBhYm91dCBhIHRoaXJkIG9mIHRoZSBvcGluaW9uIHR3ZWV0cy4gU2luY2UgdGhlIG9waW5pb24gdHdlZXRzIGFyZSB0aGUgdGhpcmQgbW9zdCBmcmVxdWVudGx5IHNlZW4gY2xhc3MgYWZ0ZXIgam9icyBhbmQgbGVhcm5pbmcsIHRoZSBtb2RlbCdzIGFjY3VyYWN5IGRlZ3JhZGVkIHNpZ25pZmljYW50bHkuIFRoZSBtb2RlbCBhbHNvIHdhcyBvbmx5IGFibGUgdG8gY29ycmVjdGx5IGNsYXNzaWZ5IG9ubHkgb25lIG91dCBvZiBmaXZlIGhlbHAgdHdlZXRzLCBhbmQgdGhyZWUgb3V0IG9mIG5pbmUgSW5kb25lc2lhIHR3ZWV0cy4NCg0KIyMjIDUuMyBBbiBTVk0gY2xhc3NpZmllciB3aXRoIGEgbGluZWFyIGtlcm5lbA0KDQpJdCBoYXMgYmVlbiBbYXJndWVkXShodHRwczovL2NhbGN1bGF0ZWRjb250ZW50LmNvbS8yMDEyLzAyLzA2L2tlcm5lbHNfcGFydF8xLykgdGhhdCBsaW5lYXIga2VybmVscyBtaWdodCBiZSBiZXR0ZXIgc3VpdGVkIHRvIHRleHQgY2xhc3NpZmljYXRpb24gdGhhbiBSQkYga2VybmVscy4gTGluZWFyIGtlcm5lbHMgYXJlIGxlc3MgY29tcHV0YXRpb25hbGx5IGV4cGVuc2l2ZSwgc28gdGhleSBhcmUgd29ydGggYSB0cnkuDQoNCiMjIyMgNS4zLjEgQW4gU1ZNIHdpdGggbGluZWFyIGtlcm5lbCBleGFtcGxlDQoNCkhlcmUgYWdhaW4gSSB3YW50ZWQgdG8gc2VlIHdoYXQgYW4gU1ZNIHRyYWluZWQgb24gbXVsdGktY2xhc3MgZGF0YSB3b3VsZCBsb29rIGxpa2UsIHRoaXMgdGltZSB1c2luZyBhIGxpbmVhciBrZXJuZWwuIEFnYWluIGFydGlmaWNpYWxseSBnZW5lcmF0ZWQgZGF0YSwgbm93IGxpbmVhcmx5IHNlcGFyYWJsZSwgd2FzIGdlbmVyYXRlZC4NCmBgYHtyLCBmaWcud2lkdGggPSA2LCBmaWcuYXNwID0gMS4wfQ0Kc2V0LnNlZWQoNTY3OCkNCngxMSA8LSBybm9ybSgxMDApDQp4MjIgPC0gcm5vcm0oMTAwKQ0KeTEgPC0gaWZlbHNlKHgyMiA8PSB4MTEgJiB4MjIgPj0gLXgxMSwgMSwgaWZlbHNlKHgyMiA+PSB4MTEgJiB4MjIgPj0gLXgxMSwgMiwgaWZlbHNlKHgyMiA8PSAteDExICYgeDIyID49IHgxMSwgMywgNCkpKQ0KcGxvdCh4MTEsIHgyMiwgY29sID0gaWZlbHNlKHgyMiA8PSB4MTEgJiB4MjIgPj0gLXgxMSwgInJlZCIsIGlmZWxzZSh4MjIgPj0geDExICYgeDIyID49IC14MTEsICJncmVlbiIsIGlmZWxzZSh4MjIgPD0gLXgxMSAmIHgyMiA+PSB4MTEsICJibHVlIiwgImdyYXkiKSkpLCBwY2ggPSAxOSwgeGxpbSA9IGMoLTIsMiksIHlsaW0gPSBjKC0yLDIpKQ0KbGluZXMoeDExLCAteDExKQ0KbGluZXMoeDExLCB4MTEpDQpgYGANCg0KTm93IGxldCdzIHRyYWluIHRoZSBTVk0gd2l0aCBsaW5lYXIga2VybmVsDQpgYGB7ciwgZmlnLndpZHRoID0gNiwgZmlnLmFzcCA9IDAuOH0NCnkxIDwtIGFzLmZhY3Rvcih5MSkNCm15X2RmMiA8LSBkYXRhLmZyYW1lKHgyMiwgeDExLCB5MSkNCm15X21vZGVsMiA8LSBzdm0oeTEgfiAuLCBkYXRhID0gbXlfZGYyLCBrZXJuZWwgPSAibGluZWFyIikNCnBsb3QobXlfbW9kZWwyLCBteV9kZjIpDQpgYGANCg0KR3JlYXQhIFRoZSBwbG90dGluZyBvZiB0aGUgU1ZNIG1vZGVsIGlzIGEgbGl0dGxlIGphZ2dlZCwgYnV0IHN0aWxsLg0KDQojIyMjIDUuMy4yIFRyYWluaW5nIHRoZSBTVk0gd2l0aCBsaW5lYXIga2VybmVsIG9uIHRoZSB0d2l0dGVyIGRhdGENCg0KTm93IGxldCdzIHNlZSBob3cgdGhlIGxpbmVhciBrZXJuZWwgZG9lcyBvbiB0aGUgdHdpdHRlciBkYXRhLiBBZ2Fpbiwgd2Ugd29uJ3QgYmUgYWJsZSB0byBzZWUgYW55IG5pZnR5IHBsb3QgaGVyZS4NCmBgYHtyfQ0Kc2V0LnNlZWQoNTY3OCkNCnR1bmVkLmxpbmVhciA8LSB0dW5lLnN2bShjbGFzc34uLCBrZXJuZWwgPSAibGluZWFyIiwgZGF0YSA9IHRyYWluU3BhcnNlLCBjb3N0ID0gMTBeKC0yOjIpKQ0KYmVzdF9jb3N0X2xpbmVhciA8LSB0dW5lZC5saW5lYXIkYmVzdC5wYXJhbWV0ZXJzJGNvc3QNCm1vZGVsLmxpbmVhci50dW5lZCA8LSBzdm0oY2xhc3N+LiwgZGF0YSA9IHRyYWluU3BhcnNlLCBrZXJuZWwgPSAibGluZWFyIiwgY29zdCA9IGJlc3RfY29zdF9saW5lYXIpDQpgYGANCg0KTGV0J3Mgc2VlIGhvdyB0aGUgdHVuZWQgU1ZNIG1vZGVsIHBlcmZvcm1zIG9uIHRoZSB0ZXN0IGRhdGEuDQpgYGB7cn0NCnN2bS50dW5lZC5saW5lYXIucHJlZCA8LSBwcmVkaWN0KG1vZGVsLmxpbmVhci50dW5lZCwgbmV3ZGF0YT10ZXN0U3BhcnNlLCB0eXBlPSJjbGFzcyIpDQpzdm0udHVuZWQubGluZWFyLnRhYmxlIDwtIHRhYmxlKHRlc3RTcGFyc2UkY2xhc3MsIHN2bS50dW5lZC5saW5lYXIucHJlZCkNCnN2bS50dW5lZC5saW5lYXIudGFibGUNCmBgYA0KDQpQZXJmb3JtYW5jZSBvZiB0aGUgdHVuZWQgbGluZWFyIFNWTSBtb2RlbCBvbiB0aGUgdGVzdCBzZXQNCmBgYHtyfQ0KYWNjdXJhY3lfdHVuZWRfbGluZWFyX3N2bSA8LSBzdW0oZGlhZyhzdm0udHVuZWQubGluZWFyLnRhYmxlKSkgLyBzdW0oc3ZtLnR1bmVkLmxpbmVhci50YWJsZSkNCmNhdCgiQWNjdXJhY3kgb2YgdHVuZWQgbGluZWFyIFNWTToiLCBhY2N1cmFjeV90dW5lZF9saW5lYXJfc3ZtKQ0KYGBgDQoNClNvIHRoZSBwZXJmb3JtYW5jZSBvZiB0aGUgbGluZWFyIGtlcm5lbCBpcyBjb21wYXJhYmxlIHRvIHRoYXQgb2YgdGhlIFJCRiBrZXJuZWwsIGlmIG5vdCBhIGxpdHRsZSBiZXR0ZXIuIFNpbmNlIGl0IGlzIGFsc28gbGVzcyBjb21wdXRhdGlvbmFsbHkgb25lcm91cywgaXQgbWlnaHQgYmUgdGhlIHByZWZlcnJlZCB3YXkgdG8gZ28uDQoNCiMjIDYuMCBTdW1tYXJ5DQoNCldlIGhhdmUgdHJpZWQgYW4gUkJGLVNWTSBhbmQgYSBsaW5lYXItU1ZNIGNsYXNzaWZpZXIgb24gYSBkYXRhc2V0IG9mIHR3ZWV0cyB0aGF0IGhhdmUgYmVlbiBjYXRlZ29yaXplZCBpbnRvIHNldmVuIGNsYXNzZXMuIFRoZSBhY2N1cmFjeSBvZiBib3RoIGNsYXNzaWZpZXJzIGlzIGp1c3Qgc2h5IG9mICQ3MFwlJC4gU2luY2UgbGluZWFyLVNWTSBpcyBtb3JlIGNvbXB1dGF0aW9uYWxseSBpbmV4cGVuc2l2ZSwgd2Ugd291bGQgcHJlZmVyIGl0LiBJZiB3ZSBoYWQgYmVlbiBhIGxpdHRsZSBsZXNzIGFtYml0aW91cyBhbmQgbHVtcGVkIHRoZSAiam9iIiwgImxlYXJuaW5nIiwgImhlbHAiLCBhbmQgIm9waW5pb24iIHR3ZWV0cyBpbnRvIGEgc2luZ2xlIGNsYXNzIGNhbGxlZCAicHJvZ3JhbW1pbmciLCBlaXRoZXIgU1ZNIHdvdWxkIGhhdmUgYWNoaWV2ZWQgYW4gYWNjdXJhY3kgaW4gdGhlIGhpZ2ggZWlnaHRpZXMgb3IgbG93IG5pbmV0aWVzLiBGb3IgZXhhbXBsZSwgdGhlIGxpbmVhciBTVk0gd291bGQgaGF2ZSBoYWQgJDI1JCBtaXNjbGFzc2lmaWVkIHR3ZWV0cyBpbiB0aGUgdGVzdCBzZXQsIG91dCBvZiAkMjUzJCB0d2VldHMuDQoNCiMjIDcuMCBSZWZlcmVuY2VzDQoNCjEuIEJlcnRzaW1hcywgRC4sIE8nSGFpciwgQS4gWyoqKlRoZSBBbmFseXRpY3MgRWRnZSoqKl0oaHR0cHM6Ly93d3cuZWR4Lm9yZy9jb3Vyc2UvYW5hbHl0aWNzLWVkZ2UtbWl0eC0xNS0wNzF4LTMpLiBTcHJpbmcgMjAxNC4gZWRYLm9yZy4NCg0KMi4gQ2hpdSBZdS1XZWkuICoqKk1hY2hpbmUgTGVhcm5pbmcgd2l0aCBSIENvb2tib29rKioqLiBCaXJtaW5naGFtOiBQYWNrdCBQdWJsaXNoaW5nLCAyMDE1LCBQREYuDQoNCjMuIEhzdSBDaGloLVdlaSwgQ2hpaC1DaHVuZyBDaGFuZywgYW5kIENoaWgtSmVuIExpbi4gWyoqKkEgUHJhY3RpY2FsIEd1aWRlIHRvIFN1cHBvcnQgVmVjdG9yIENsYXNzaWZpY2F0aW9uKioqXShodHRwczovL3d3dy5jc2llLm50dS5lZHUudHcvfmNqbGluL3BhcGVycy9ndWlkZS9ndWlkZS5wZGYpDQoNCjQuIGFkaXR5YXNocm0yMSBhbmQgYW5vbi4gWyoqKkVycm9yOiBpbmhlcml0cyhkb2MsIOKAnFRleHREb2N1bWVudOKAnSkgaXMgbm90IFRSVUUgaW4gUioqKl0oaHR0cHM6Ly9kaXNjdXNzLmFuYWx5dGljc3ZpZGh5YS5jb20vdC9lcnJvci1pbmhlcml0cy1kb2MtdGV4dGRvY3VtZW50LWlzLW5vdC10cnVlLWluLXIvMTA3OCkuDQoNCjUuIFNUSERBLiBbKioqVGV4dCBtaW5pbmcgYW5kIHdvcmQgY2xvdWQgZnVuZGFtZW50YWxzIGluIFIgOiA1IHNpbXBsZSBzdGVwcyB5b3Ugc2hvdWxkIGtub3cqKipdKGh0dHA6Ly93d3cuc3RoZGEuY29tL2VuZ2xpc2gvd2lraS90ZXh0LW1pbmluZy1hbmQtd29yZC1jbG91ZC1mdW5kYW1lbnRhbHMtaW4tci01LXNpbXBsZS1zdGVwcy15b3Utc2hvdWxkLWtub3cpLg0KDQo2LiBrbmIgYW5kIEFuZHJpZS4gWyoqKlI6IGFkZCB0aXRsZSB0byB3b3JkY2xvdWQgZ3JhcGhpY3MgLyBwbmcqKipdKGh0dHBzOi8vc3RhY2tvdmVyZmxvdy5jb20vcXVlc3Rpb25zLzE1MjI0OTEzL3ItYWRkLXRpdGxlLXRvLXdvcmRjbG91ZC1ncmFwaGljcy1wbmcpLg0KDQo3LiBTYW5kcmEgU2NobGljaHRpbmcgYW5kIGJuYXVsLiBbKioqUGxvdCB0d28gZ3JhcGhzIGluIHNhbWUgcGxvdCBpbiBSKioqXShodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy8yNTY0MjU4L3Bsb3QtdHdvLWdyYXBocy1pbi1zYW1lLXBsb3QtaW4tcikNCg0KOC4gTW9uYSBKYWxhbCBhbmQgR3JlZ29yLiBbKioqZHJhd2luZyBjaXJjbGUgaW4gUioqKl0oaHR0cHM6Ly9zdGFja292ZXJmbG93LmNvbS9xdWVzdGlvbnMvMjIyNjU3MDQvZHJhd2luZy1jaXJjbGUtaW4tcikNCg0KOS4gR2hvc2UsIEFiaGlzaGVrLiBbKioqU3VwcG9ydCBWZWN0b3IgTWFjaGluZSAoU1ZNKSBUdXRvcmlhbCoqKl0oaHR0cHM6Ly9ibG9nLnN0YXRzYm90LmNvL3N1cHBvcnQtdmVjdG9yLW1hY2hpbmVzLXR1dG9yaWFsLWMxNjE4ZTYzNWU5MykNCg0KMTAuIEpjcm93MDYgYW5kIHRpbSByaWZmZS4gWyoqKlIgY29sb3Igc2NhdHRlciBwbG90IHBvaW50cyBiYXNlZCBvbiB2YWx1ZXMqKipdKGh0dHBzOi8vc3RhY2tvdmVyZmxvdy5jb20vcXVlc3Rpb25zLzE3NTUxMTkzL3ItY29sb3Itc2NhdHRlci1wbG90LXBvaW50cy1iYXNlZC1vbi12YWx1ZXMpDQoNCjExLiBDaGFybGVzIE1hcnRpbi4gWyoqKktFUk5FTFMgUEFSVCAxOiBXSEFUIElTIEFOIFJCRiBLRVJORUw/IFJFQUxMWT8qKipdKGh0dHBzOi8vY2FsY3VsYXRlZGNvbnRlbnQuY29tLzIwMTIvMDIvMDYva2VybmVsc19wYXJ0XzEvKQ0KDQoNCmBgYHtyLCBldmFsPUZBTFNFLCBmaWcuYXNwPTEuMCwgZmlnLndpZHRoPTYsIGluY2x1ZGU9RkFMU0V9DQojIyBLZWVwaW5nIHRoaXMgb25lIGIvYyBJIGRvbid0IHdhbnQgdG8gbG9zZSB0aGUgd29yayBvbiBwb2x5bm9taWFsDQpzZXQuc2VlZCg1Njc4KQ0KeDMxIDwtIHJ1bmlmKDIwMCwgbWluID0gLTEsIG1heCA9IDEpDQp4MzIgPC0gcnVuaWYoMjAwLCBtaW4gPSAtMSwgbWF4ID0gMSkNCnkzMSA8LSBpZmVsc2UoeDMyID49IHgzMV4yLCAxLCBpZmVsc2UoeDMyIDw9IHgzMV4yICYgeDMyID49IHgzMV4yIC0gMSwgMiwgMykpDQpwbG90KHgzMSwgeDMyLCBjb2wgPSBpZmVsc2UoeDMyID49IHgzMV4yLCAicmVkIiwgaWZlbHNlKHgzMiA8PSB4MzFeMiAmIHgzMiA+PSB4MzFeMiAtIDEsICJncmVlbiIsICJibHVlIikpLCBwY2ggPSAxOSkNCmN1cnZlKHheMiwgYWRkID0gVFJVRSkNCmN1cnZlKHheMiAtIDEsIGFkZCA9IFRSVUUpDQpgYGANCg0KYGBge3IsIGV2YWw9RkFMU0UsIGZpZy5hc3A9MC44LCBmaWcud2lkdGg9NiwgaW5jbHVkZT1GQUxTRX0NCiMjIEtlZXBpbmcgdGhpcyBvbmUgYi9jIEkgZG9uJ3Qgd2FudCB0byBsb3NlIHRoZSB3b3JrIG9uIHBvbHlub21pYWwNCnkzMSA8LSBhcy5mYWN0b3IoeTMxKQ0KbXlfZGYzIDwtIGRhdGEuZnJhbWUoeDMyLCB4MzEsIHkzMSkNCm15X21vZGVsMyA8LSBzdm0oeTMxIH4gLiwgZGF0YSA9IG15X2RmMywga2VybmVsID0gInBvbHlub21pYWwiLCBjb2VmMCA9IDIpDQpwbG90KG15X21vZGVsMywgbXlfZGYzKQ0KYGBgDQoNCmBgYHtyLCBldmFsPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQ0KIyMgS2VlcGluZyB0aGlzIG9uZSBiL2MgSSBkb24ndCB3YW50IHRvIGxvc2UgdGhlIHdvcmsgb24gcG9seW5vbWlhbA0Kc2V0LnNlZWQoNTY3OCkNCnR1bmVkLnBvbHkgPC0gdHVuZS5zdm0oY2xhc3N+Liwga2VybmVsID0gInBvbHlub21pYWwiLCBkYXRhID0gdHJhaW5TcGFyc2UsIGdhbW1hID0gMTBeKC0yOi0xKSwgZGVncmVlID0gKDE6MyksIGNvZWYwID0gMSwgY29zdCA9IDEwXigwOjIpKQ0Kc3VtbWFyeSh0dW5lZC5wb2x5KQ0KYGBgDQoNCg0KYGBge3IsIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9DQojIyBLZWVwaW5nIHRoaXMgb25lIGIvYyBJIGRvbid0IHdhbnQgdG8gbG9zZSB0aGUgd29yayBvbiBwb2x5bm9taWFsDQptb2RlbC5wb2x5LnR1bmVkIDwtIHN2bShjbGFzc34uLCBkYXRhID0gdHJhaW5TcGFyc2UsIGtlcm5lbCA9ICJwb2x5bm9taWFsIiwgZGVncmVlID0gMiwgZ2FtbWEgPSAwLjAxLCBjb2VmMCA9IDEsIGNvc3QgPSAxKQ0Kc3VtbWFyeShtb2RlbC5wb2x5LnR1bmVkKQ0KYGBgDQoNCg0KYGBge3IsIGV2YWw9RkFMU0UsIGluY2x1ZGU9RkFMU0V9DQojIyBLZWVwaW5nIHRoaXMgb25lIGIvYyBJIGRvbid0IHdhbnQgdG8gbG9zZSB0aGUgd29yayBvbiBwb2x5bm9taWFsDQpzdm0udHVuZWQucG9seS5wcmVkIDwtIHByZWRpY3QobW9kZWwucG9seS50dW5lZCwgbmV3ZGF0YT10ZXN0U3BhcnNlLCB0eXBlPSJjbGFzcyIpDQpzdm0udHVuZWQucG9seS50YWJsZSA8LSB0YWJsZSh0ZXN0U3BhcnNlJGNsYXNzLCBzdm0udHVuZWQucG9seS5wcmVkKQ0Kc3ZtLnR1bmVkLnBvbHkudGFibGUNCmBgYA0KDQpgYGB7ciwgZXZhbD1GQUxTRSwgaW5jbHVkZT1GQUxTRX0NCiMjIEtlZXBpbmcgdGhpcyBvbmUgYi9jIEkgZG9uJ3Qgd2FudCB0byBsb3NlIHRoZSB3b3JrIG9uIHBvbHlub21pYWwNCmFjY3VyYWN5X3R1bmVkX3BvbHlfc3ZtIDwtIHN1bShkaWFnKHN2bS50dW5lZC5wb2x5LnRhYmxlKSkgLyBzdW0oc3ZtLnR1bmVkLnBvbHkudGFibGUpDQpjYXQoIkFjY3VyYWN5IG9mIHR1bmVkIHBvbHlub21pYWwgU1ZNOiIsIGFjY3VyYWN5X3R1bmVkX3BvbHlfc3ZtKQ0KYGBg