I. CONTENT FILTERING

A. Item Profile

We start with a set of item characteristics. In our example we used whether the movie had Arnold Schwarzenegger, Julia Roberts as well as some measure of surprise in the script.

row_names<-c("AS", "JR", "surprise")
col_names<-c("PW", "TR", "EB", "T2", "P")

item <- matrix( c(0,   1,   0,   1, 1,
                                    1,   0,   1,   0, 0,                       
                                  0.1, 0.4, 0.1, 0, 0.1),
                                byrow = TRUE, nrow = 3, ncol = 5,
                                dimnames = list(row_names, col_names))
item %>% 
  kbl() %>%
  kable_styling()
PW TR EB T2 P
AS 0.0 1.0 0.0 1 1.0
JR 1.0 0.0 1.0 0 0.0
surprise 0.1 0.4 0.1 0 0.1

We also have Adam’s ratings of the items. We normalize them so that it is above/below his average rating.

rating <- matrix(c(3,1,5,2,4),nrow=1,ncol=5)
rating_m <- rating-mean(rating)

rating_m %>% 
  kbl() %>%
  kable_styling()
0 -2 2 -1 1

B. User Profile

To create the user profile, we need to see how much the users ratings change with the characteristics. The following tells us how much ratings change with attributes:

# t() means taking the transpose of a matrix, M'
user <- item %*% t(rating_m) / rowSums(item) ## This is the FORMULA in slides
user %>% 
  kbl() %>%
  kable_styling()
AS -0.667
JR 1.000
surprise -0.714

C. Similarity Prediction

To make predictions, we calculate the similarity between the user’s characteristic preferences and the characteristics of the items. The closer these two are, the better the fit.

row_names <- c("AS", "JR", "surprise")
col_names <- c("TL", "NH")

new_item <- matrix(c(1,0,
              0,1,
              .1,0), byrow = TRUE, nrow = 3, ncol = 2, dimnames=list(row_names,col_names))
new_item %>% 
  kbl() %>%
  kable_styling()
TL NH
AS 1.0 0
JR 0.0 1
surprise 0.1 0
CS = t(new_item) %*% user / (sqrt(colSums(new_item^2))*sqrt(sum(user^2))) ## FORMULA in slides
CS %>% 
  kbl() %>%
  kable_styling() ## We recomend more Noting Hill, as is closer to 1.
TL -0.525
NH 0.715

D. Other Example

Now, Consider the item and rating matrix below:

row_names<-c("Funny", "Romant", "Suspense", "Dark")
col_names<-c("Sharp Obj", "Arrested Dev", "Arbitrage", "Margin C", "Bojack", "Orphan B", "Hinterland")

item <- matrix(c(0,1,0,1,1,1,0,
                 1,1,0,0,1,0,0,
                 1,1,1,0,1,0,1,
                 1,0,1,1,0,1,1), 
               byrow = TRUE, nrow = 4, ncol = 7, dimnames=list(row_names,col_names))

item %>% 
  kbl() %>%
  kable_styling()
Sharp Obj Arrested Dev Arbitrage Margin C Bojack Orphan B Hinterland
Funny 0 1 0 1 1 1 0
Romant 1 1 0 0 1 0 0
Suspense 1 1 1 0 1 0 1
Dark 1 0 1 1 0 1 1
rating <- matrix(c(4,3,4,5,3),nrow=1,ncol=5)
rating %>% 
  kbl() %>%
  kable_styling()
4 3 4 5 3

Calculate the cosine similarity for the 2 movies and decide which to recommend.

User Profile

user <- item[,1:5] %*% t(rating) / rowSums(item) ## This is the FORMULA in slides

user %>% 
  kbl() %>%
  kable_styling()
Funny 2.75
Romant 3.33
Suspense 2.80
Dark 2.60

II. COLLABORATIVE FILTERING

From lecture we gave an example of 7 users and 6 items. We are trying to predict whether to recommend Predator or Notting Hill to Adam based on his similarity with others.

row_names<-c("A", "B", "C", "D", "E", "F", "G")
col_names<-c("PW", "TR", "EB", "T2", "P", "NH")

util <- matrix(c(2,5,4,2,NA, NA,
              5,1,2,NA,1,NA,
              5,5,5,5,5,5,
              2,5,NA,3,NA,NA,
              5,4,5,3,NA,5,
              1,5,NA,NA,NA,1,
              2,NA,5,NA,5,NA),byrow = TRUE, nrow = 7, ncol = 6, dimnames=list(row_names,col_names))
util %>% 
  kbl() %>%
  kable_styling()
PW TR EB T2 P NH
A 2 5 4 2 NA NA
B 5 1 2 NA 1 NA
C 5 5 5 5 5 5
D 2 5 NA 3 NA NA
E 5 4 5 3 NA 5
F 1 5 NA NA NA 1
G 2 NA 5 NA 5 NA

User Based

Let’s take a look at user-based collaborative filtering. We’ll use simple correlations between users to see who is more similar to A.

cor(t(util), use="pairwise.complete.obs") %>% 
  kbl() %>%
  kable_styling()
## Warning in cor(t(util), use = "pairwise.complete.obs"): the standard deviation is zero
A B C D E F G
A 1.000 -0.996 NA 0.945 0.174 1 1.000
B -0.996 1.000 NA -1.000 0.693 -1 -0.971
C NA NA NA NA NA NA NA
D 0.945 -1.000 NA 1.000 -0.327 1 NA
E 0.174 0.693 NA -0.327 1.000 -1 NA
F 1.000 -1.000 NA 1.000 -1.000 1 NA
G 1.000 -0.971 NA NA NA NA 1.000

From this we see that only B, F and G are relevant: We focus only on the 3 most similar customers, B F and G.

m <- cor(t(util), use="pairwise.complete.obs") # re-run the correlation matrix
## Warning in cor(t(util), use = "pairwise.complete.obs"): the standard deviation is zero
# The relevant row
m[row=c("B","F","G"), col=c("A")] %>% 
  kbl() %>%
  kable_styling()
x
B -0.996
F 1.000
G 1.000

We normalize the ratings and multiply the correlations by their ratings of the movies in question, P and NH. Then we average to get the predicted ratings of Adam.

util_n <- util - rowMeans(util, na.rm=TRUE) #normalize

## Multiply the correlations by their ratings of the movies in question, P and N
predm <- m[row=c("B","F","G"),col=c("A")]*util_n[row=c("B","F","G"), col=c("P","NH")]
## Take the average
pred <- colMeans(predm, na.rm=TRUE)
pred %>% 
  kbl() %>%
  kable_styling()
x
P 1.12
NH -1.33

Adam’s ratings would be 1.122 higher than average for P and -1.333 for NH.

Item Based

Now for the item based filtering. We do the correlations across columns instead of rows.

m <- cor(util, use="pairwise.complete.obs")

Focus on last two columns. We focus on the movies that have either perfect positive or negative correlation. For P that is TR and EB, for NH that is PW.

m <- m[row=c("PW", "TR","EB"), col=c("P", "NH")]

# make NA anything less than 1
m[abs(m)<1] <- NA
m %>% 
  kbl() %>%
  kable_styling()
P NH
PW NA 1
TR 1 NA
EB 1 NA

The prediction is the product of the correlation between the target movie and the other movies and Adam’s normalized reviews for the other movies:

predm<-m*util_n[row=c("A"),col=c("PW", "TR","EB")]
predm %>% 
  kbl() %>%
  kable_styling()
P NH
PW NA -1.25
TR 1.75 NA
EB 0.75 NA
pred<-colMeans(predm, na.rm = TRUE)
pred %>% 
  kbl() %>%
  kable_styling()
x
P 1.25
NH -1.25

Adam’s ratings would be 1.25 higher than average for P and -1.25 than average for NH.

III. REAL DATA EXAMPLES

0. Data

data("MovieLense")
MovieLense
## 943 x 1664 rating matrix of class 'realRatingMatrix' with 99392 ratings.
#getRatingMatrix(MovieLense)[1:10,1:5]
as(MovieLense, "matrix")[1:10, 1:5]
##    Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
## 1                 5                3                 4                 3              3
## 2                 4               NA                NA                NA             NA
## 3                NA               NA                NA                NA             NA
## 4                NA               NA                NA                NA             NA
## 5                 4                3                NA                NA             NA
## 6                 4               NA                NA                NA             NA
## 7                NA               NA                NA                 5             NA
## 8                NA               NA                NA                NA             NA
## 9                NA               NA                NA                NA             NA
## 10                4               NA                NA                 4             NA
test <- as(MovieLense, "matrix")[1:10,]
image(MovieLense)

count <- colCounts(MovieLense)
head(sort(count, decreasing = TRUE))
##            Star Wars (1977)              Contact (1997)                Fargo (1996) 
##                         583                         509                         508 
##   Return of the Jedi (1983)            Liar Liar (1997) English Patient, The (1996) 
##                         507                         485                         481
hist(colCounts(MovieLense), xlab="number of reviews", main = "number of reviews per movie")

hist(colMeans(MovieLense), xlab="average movie ratings", main="", breaks=50)

1. Content Filtering

A. Build Item Profile

There are a bunch of meta-characteristics available. We’ll use the genres: from unknown to Western as our item characteristics.

head(MovieLenseMeta)
##                                                  title year
## 1                                     Toy Story (1995) 1995
## 2                                     GoldenEye (1995) 1995
## 3                                    Four Rooms (1995) 1995
## 4                                    Get Shorty (1995) 1995
## 5                                       Copycat (1995) 1995
## 6 Shanghai Triad (Yao a yao yao dao waipo qiao) (1995) 1995
##                                                            url unknown Action Adventure Animation
## 1        http://us.imdb.com/M/title-exact?Toy%20Story%20(1995)       0      0         0         1
## 2          http://us.imdb.com/M/title-exact?GoldenEye%20(1995)       0      1         1         0
## 3       http://us.imdb.com/M/title-exact?Four%20Rooms%20(1995)       0      0         0         0
## 4       http://us.imdb.com/M/title-exact?Get%20Shorty%20(1995)       0      1         0         0
## 5            http://us.imdb.com/M/title-exact?Copycat%20(1995)       0      0         0         0
## 6 http://us.imdb.com/Title?Yao+a+yao+yao+dao+waipo+qiao+(1995)       0      0         0         0
##   Children's Comedy Crime Documentary Drama Fantasy Film-Noir Horror Musical Mystery Romance Sci-Fi
## 1          1      1     0           0     0       0         0      0       0       0       0      0
## 2          0      0     0           0     0       0         0      0       0       0       0      0
## 3          0      0     0           0     0       0         0      0       0       0       0      0
## 4          0      1     0           0     1       0         0      0       0       0       0      0
## 5          0      0     1           0     1       0         0      0       0       0       0      0
## 6          0      0     0           0     1       0         0      0       0       0       0      0
##   Thriller War Western
## 1        0   0       0
## 2        1   0       0
## 3        1   0       0
## 4        0   0       0
## 5        1   0       0
## 6        0   0       0
item <- as.matrix(subset(MovieLenseMeta, select = -c(title, year, url)))

B. Build User Profile

We’ll take user 1 as our “Adam”, our user on which to build our content filtering system. We normalize his ratings by subtracting off the mean. We create an index, non_miss of the ratings he gives.

rating <- as(MovieLense, "matrix")[1,]

rating_m <- rating-mean(rating,na.rm=TRUE) ## normalize

non_miss <- !is.na(rating_m)
miss <- is.na(rating_m)

We calculate his user profile using the formula (slides). Only difference is that the item matrix is the opposite from the above example: movies are rows and attributes are columns.

So we change the matrix multiplication: transpose item matrix and take column sums rather than row sums.

user <- (t(item[non_miss,]) %*% rating_m[non_miss]) / colSums(item[non_miss, ])
user
##                 [,1]
## unknown      0.39483
## Action      -0.27183
## Adventure   -0.67659
## Animation   -0.27183
## Children's  -1.40517
## Comedy      -0.13264
## Crime       -0.16517
## Documentary  1.19483
## Drama        0.30993
## Fantasy     -0.10517
## Film-Noir    1.39483
## Horror      -0.14363
## Musical     -0.68209
## Mystery     -0.00517
## Romance      0.30181
## Sci-Fi       0.39483
## Thriller     0.01022
## War          0.07483
## Western      0.06150

C. Similarity Prediction

We take all of the movies he/she has not seen, and make our cosine similarity predictions on them.

names <- as.matrix(subset(MovieLenseMeta, select = c(title)))
new_item <- item[miss,]
new_names <- names[miss,]

We apply the formula:

CS = (new_item) %*% user / (sqrt(rowSums(new_item^2))*sqrt(sum(user^2)))
hist(CS, main = "histogram of cosine similarity with unseen movies", xlab="Cosine Similarity")

The top 6 movies predicted (of the 1393 not rated movies) are:

new_names[head(order(CS, decreasing = TRUE))]
##                                                           488 
##                                         "Sunset Blvd. (1950)" 
##                                                          1476 
##                                             "Raw Deal (1948)" 
##                                                          1582 
##                                                "T-Men (1947)" 
##                                                           320 
## "Paradise Lost: The Child Murders at Robin Hood Hills (1996)" 
##                                                           360 
##                                           "Wonderland (1997)" 
##                                                           634 
##                    "Microcosmos: Le peuple de l'herbe (1996)"

2. Non-Personalized Recommendations: Popularity

Popular normalizes the ratings by user, and takes the average across users. It doesn’t recommend something to someone who has already rated it. But it starts at the top of the list and goes down.

row_names<-c("A", "B", "C", "D", "E", "F", "G")
col_names<-c("PW", "TR", "EB", "T2", "P", "NH")

util <- matrix(c(2,5,4,2,NA, NA,
              5,1,2,NA,1,NA,
              5,5,5,5,5,5,
              2,5,NA,3,NA,NA,
              5,4,5,3,NA,5,
              1,5,NA,NA,NA,1,
              2,NA,5,NA,5,NA),byrow = TRUE, nrow = 7, ncol = 6, dimnames=list(row_names,col_names))
util %>% 
  kbl() %>%
  kable_styling()
PW TR EB T2 P NH
A 2 5 4 2 NA NA
B 5 1 2 NA 1 NA
C 5 5 5 5 5 5
D 2 5 NA 3 NA NA
E 5 4 5 3 NA 5
F 1 5 NA NA NA 1
G 2 NA 5 NA 5 NA
util_n <-util-rowMeans(util, na.rm=TRUE)

In our example above in the collaborative filtering part, it would make TR the first, EB the second, etc. If someone had already watched TR, the first recommendation would be EB, then P, etc.

colMeans(util_n,na.rm = TRUE) %>% 
  kbl() %>%
  kable_styling()
x
PW -0.367
TR 0.739
EB 0.420
T2 -0.746
P -0.083
NH -0.244
test<- as(util, "realRatingMatrix")
test_recom<-Recommender(test, method = "POPULAR")
test_recom@model$topN@items
## [[1]]
## [1] 2 3 5 6 1 4
test_pred<-predict(test_recom, test[1,],type="ratings")

as(test_pred,"matrix") %>% 
  kbl() %>%
  kable_styling()
PW TR EB T2 P NH
A NA NA NA NA 3.17 3.01

Adam’s average review is 3.25. The average rating of P is -0.083 compared to the average. Hence the prediction of the popular model is these two quantities added, 3.167.

3. Collaborative Filtering

0. MovieLense “Method

set.seed(19103)
es <- evaluationScheme(MovieLense, 
  method="split", train=0.9, given=15)
## as(<dgCMatrix>, "dgTMatrix") is deprecated since Matrix 1.5-0; do as(., "TsparseMatrix") instead
es
## Evaluation scheme with 15 items given
## Method: 'split' with 1 run(s).
## Training set proportion: 0.900
## Good ratings: NA
## Data set: 943 x 1664 rating matrix of class 'realRatingMatrix' with 99392 ratings.
train <- getData(es, "train"); train
## 848 x 1664 rating matrix of class 'realRatingMatrix' with 90673 ratings.
test_known <- getData(es, "known"); test_known
## 95 x 1664 rating matrix of class 'realRatingMatrix' with 1425 ratings.
test_unknown <- getData(es, "unknown"); test_unknown
## 95 x 1664 rating matrix of class 'realRatingMatrix' with 7294 ratings.
popular <-Recommender(train, "POPULAR")
## create predictions for the test users using known ratings
pred_pop <- predict(popular, test_known, type="ratings"); pred_pop
## 95 x 1664 rating matrix of class 'realRatingMatrix' with 155897 ratings.
## evaluate recommendations on "unknown" ratings
acc_pop <- calcPredictionAccuracy(pred_pop, test_unknown);
as(acc_pop,"matrix") %>% 
  kbl() %>%
  kable_styling()
RMSE 0.989
MSE 0.977
MAE 0.780
as(test_unknown, "matrix")[1:8,1:5]
##    Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
## 10                4               NA                NA                 4             NA
## 35               NA               NA                NA                NA             NA
## 53               NA               NA                NA                NA             NA
## 65                3               NA                NA                NA             NA
## 66                3               NA                NA                NA             NA
## 78               NA               NA                NA                NA             NA
## 79                4               NA                NA                NA             NA
## 94                4               NA                NA                 4             NA
as(pred_pop, "matrix")[1:8,1:5]
##    Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
## 10             4.43             3.83              3.72              4.09           3.99
## 35             3.56             2.97              2.85              3.22           3.12
## 53             4.03             3.43              3.32              3.69           3.59
## 65             4.43             3.83              3.72              4.09           3.99
## 66             3.16             2.57              2.45              2.82           2.72
## 78             3.56             2.97              2.85              3.22           3.12
## 79             4.23             3.63              3.52              3.89           3.79
## 94             3.56             2.97              2.85              3.22           3.12

A. User-Based

Now we’ll use user-based collaborative filtering. We’ll use (pearson) correlation to determine the similarity across users.

And we’ll use the 30 most similar users in making our recommendation.

UBCF <- Recommender(train, "UBCF",
                        param=list(method="pearson",nn=30))

## create predictions for the test users using known ratings
pred_ub <- predict(UBCF, test_known, type="ratings"); pred_ub
## 95 x 1664 rating matrix of class 'realRatingMatrix' with 75118 ratings.
## evaluate recommendations on "unknown" ratings
acc_ub <- calcPredictionAccuracy(pred_ub, test_unknown);
acc <- rbind(POP=acc_pop, UBCF = acc_ub); acc
##       RMSE   MSE  MAE
## POP  0.989 0.977 0.78
## UBCF 1.118 1.251 0.88
as(test_unknown, "matrix")[1:8,1:5]
##    Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
## 10                4               NA                NA                 4             NA
## 35               NA               NA                NA                NA             NA
## 53               NA               NA                NA                NA             NA
## 65                3               NA                NA                NA             NA
## 66                3               NA                NA                NA             NA
## 78               NA               NA                NA                NA             NA
## 79                4               NA                NA                NA             NA
## 94                4               NA                NA                 4             NA
as(pred_ub, "matrix")[1:8,1:5]
##    Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
## 10             4.47             3.80              3.88              4.38           4.22
## 35             3.43             2.75              1.74              3.45           3.13
## 53             4.10               NA                NA              3.70           2.50
## 65             3.43               NA              4.39                NA           4.21
## 66             3.25             2.51                NA              3.55           4.21
## 78             3.85             2.69                NA              3.47           2.96
## 79             4.22             3.72              5.09              3.91           3.29
## 94             3.66             2.56              2.22              2.92           2.88

UBCF has higher error metrics than popularity, indicating worse fit.

B. Item-Based

Here we use item-based collaborative filtering, using peason correlation to determine similarity across items. And we use the 30 most similiar items.

IBCF <- Recommender(train, "IBCF",
                        param=list(method="pearson",k=30))

pred_ib <- predict(IBCF, test_known, type="ratings")

acc_ib <- calcPredictionAccuracy(pred_ib, test_unknown) 

acc <- rbind(POP=acc_pop, UBCF = acc_ub, IBCF = acc_ib); acc
##       RMSE   MSE  MAE
## POP  0.989 0.977 0.78
## UBCF 1.118 1.251 0.88
## IBCF 1.507 2.271 1.16

Note the error metric is yet worse for IBCF.

LS0tDQp0aXRsZTogIlR1dG9yaWFsIDU6IFJlY29tZW5kYXRpb24gU3lzdGVtcyINCmRhdGU6ICIyMDIzLTAxLTIyIg0Kb3V0cHV0OiANCiAgaHRtbF9kb2N1bWVudDoNCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCiAgICBjb2RlX2Rvd25sb2FkOiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQpvcHRpb25zKCJzY2lwZW4iPSAyMDAsICJkaWdpdHMiPTMsIHdpZHRoID0gMTAwKQ0KbGlicmFyeShrYWJsZUV4dHJhKQ0KbGlicmFyeShyZWNvbW1lbmRlcmxhYikNCnJtKGxpc3Q9bHMoKSkNCmBgYA0KDQojIEkuIENPTlRFTlQgRklMVEVSSU5HDQoNCiMjIEEuIEl0ZW0gUHJvZmlsZQ0KDQpXZSBzdGFydCB3aXRoIGEgc2V0IG9mICoqaXRlbSBjaGFyYWN0ZXJpc3RpY3MqKi4gSW4gb3VyIGV4YW1wbGUgd2UgdXNlZCB3aGV0aGVyIHRoZSBtb3ZpZSBoYWQgKkFybm9sZCBTY2h3YXJ6ZW5lZ2dlciosICpKdWxpYSBSb2JlcnRzKiBhcyB3ZWxsIGFzIHNvbWUgbWVhc3VyZSBvZiAqc3VycHJpc2UqIGluIHRoZSBzY3JpcHQuDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0Kcm93X25hbWVzPC1jKCJBUyIsICJKUiIsICJzdXJwcmlzZSIpDQpjb2xfbmFtZXM8LWMoIlBXIiwgIlRSIiwgIkVCIiwgIlQyIiwgIlAiKQ0KDQppdGVtIDwtIG1hdHJpeCggYygwLCAgIDEsICAgMCwgICAxLCAxLA0KCQkJCQkJCQkJMSwgICAwLCAgIDEsICAgMCwgMCwJCQkJCSAgIA0KCQkJCQkJCQkgIDAuMSwgMC40LCAwLjEsIDAsIDAuMSksDQoJCQkJCQkJCWJ5cm93ID0gVFJVRSwgbnJvdyA9IDMsIG5jb2wgPSA1LA0KCQkJCQkJCQlkaW1uYW1lcyA9IGxpc3Qocm93X25hbWVzLCBjb2xfbmFtZXMpKQ0KaXRlbSAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCmBgYA0KDQpXZSBhbHNvIGhhdmUgQWRhbSdzIHJhdGluZ3Mgb2YgdGhlIGl0ZW1zLiBXZSBub3JtYWxpemUgdGhlbSBzbyB0aGF0IGl0IGlzIGFib3ZlL2JlbG93IGhpcyBhdmVyYWdlIHJhdGluZy4NCg0KYGBge3IsIHdhcm5pbmc9RkFMU0V9DQpyYXRpbmcgPC0gbWF0cml4KGMoMywxLDUsMiw0KSxucm93PTEsbmNvbD01KQ0KcmF0aW5nX20gPC0gcmF0aW5nLW1lYW4ocmF0aW5nKQ0KDQpyYXRpbmdfbSAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCmBgYA0KDQojIyBCLiBVc2VyIFByb2ZpbGUNCg0KVG8gY3JlYXRlIHRoZSB1c2VyIHByb2ZpbGUsIHdlIG5lZWQgdG8gc2VlICpob3cgbXVjaCB0aGUgdXNlcnMgcmF0aW5ncyBjaGFuZ2Ugd2l0aCB0aGUgY2hhcmFjdGVyaXN0aWNzLiogVGhlIGZvbGxvd2luZyB0ZWxscyB1cyBob3cgbXVjaCByYXRpbmdzIGNoYW5nZSB3aXRoIGF0dHJpYnV0ZXM6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0KIyB0KCkgbWVhbnMgdGFraW5nIHRoZSB0cmFuc3Bvc2Ugb2YgYSBtYXRyaXgsIE0nDQp1c2VyIDwtIGl0ZW0gJSolIHQocmF0aW5nX20pIC8gcm93U3VtcyhpdGVtKSAjIyBUaGlzIGlzIHRoZSBGT1JNVUxBIGluIHNsaWRlcw0KdXNlciAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCmBgYA0KDQojIyBDLiBTaW1pbGFyaXR5IFByZWRpY3Rpb24NCg0KVG8gbWFrZSBwcmVkaWN0aW9ucywgd2UgY2FsY3VsYXRlIHRoZSBzaW1pbGFyaXR5IGJldHdlZW4gdGhlIHVzZXIncyBjaGFyYWN0ZXJpc3RpYyBwcmVmZXJlbmNlcyBhbmQgdGhlIGNoYXJhY3RlcmlzdGljcyBvZiB0aGUgaXRlbXMuIFRoZSBjbG9zZXIgdGhlc2UgdHdvIGFyZSwgdGhlIGJldHRlciB0aGUgZml0Lg0KDQpgYGB7cn0NCnJvd19uYW1lcyA8LSBjKCJBUyIsICJKUiIsICJzdXJwcmlzZSIpDQpjb2xfbmFtZXMgPC0gYygiVEwiLCAiTkgiKQ0KDQpuZXdfaXRlbSA8LSBtYXRyaXgoYygxLDAsDQogICAgICAgICAgICAgIDAsMSwNCiAgICAgICAgICAgICAgLjEsMCksIGJ5cm93ID0gVFJVRSwgbnJvdyA9IDMsIG5jb2wgPSAyLCBkaW1uYW1lcz1saXN0KHJvd19uYW1lcyxjb2xfbmFtZXMpKQ0KbmV3X2l0ZW0gJT4lIA0KICBrYmwoKSAlPiUNCiAga2FibGVfc3R5bGluZygpDQpgYGANCg0KYGBge3J9DQpDUyA9IHQobmV3X2l0ZW0pICUqJSB1c2VyIC8gKHNxcnQoY29sU3VtcyhuZXdfaXRlbV4yKSkqc3FydChzdW0odXNlcl4yKSkpICMjIEZPUk1VTEEgaW4gc2xpZGVzDQpDUyAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkgIyMgV2UgcmVjb21lbmQgbW9yZSBOb3RpbmcgSGlsbCwgYXMgaXMgY2xvc2VyIHRvIDEuDQpgYGANCg0KIyMgRC4gT3RoZXIgRXhhbXBsZQ0KDQpOb3csIENvbnNpZGVyIHRoZSBpdGVtIGFuZCByYXRpbmcgbWF0cml4IGJlbG93Og0KDQpgYGB7cn0NCnJvd19uYW1lczwtYygiRnVubnkiLCAiUm9tYW50IiwgIlN1c3BlbnNlIiwgIkRhcmsiKQ0KY29sX25hbWVzPC1jKCJTaGFycCBPYmoiLCAiQXJyZXN0ZWQgRGV2IiwgIkFyYml0cmFnZSIsICJNYXJnaW4gQyIsICJCb2phY2siLCAiT3JwaGFuIEIiLCAiSGludGVybGFuZCIpDQoNCml0ZW0gPC0gbWF0cml4KGMoMCwxLDAsMSwxLDEsMCwNCiAgICAgICAgICAgICAgICAgMSwxLDAsMCwxLDAsMCwNCiAgICAgICAgICAgICAgICAgMSwxLDEsMCwxLDAsMSwNCiAgICAgICAgICAgICAgICAgMSwwLDEsMSwwLDEsMSksIA0KICAgICAgICAgICAgICAgYnlyb3cgPSBUUlVFLCBucm93ID0gNCwgbmNvbCA9IDcsIGRpbW5hbWVzPWxpc3Qocm93X25hbWVzLGNvbF9uYW1lcykpDQoNCml0ZW0gJT4lIA0KICBrYmwoKSAlPiUNCiAga2FibGVfc3R5bGluZygpDQpgYGANCg0KYGBge3J9DQpyYXRpbmcgPC0gbWF0cml4KGMoNCwzLDQsNSwzKSxucm93PTEsbmNvbD01KQ0KcmF0aW5nICU+JSANCiAga2JsKCkgJT4lDQogIGthYmxlX3N0eWxpbmcoKQ0KYGBgDQoNCkNhbGN1bGF0ZSB0aGUgY29zaW5lIHNpbWlsYXJpdHkgZm9yIHRoZSAyIG1vdmllcyBhbmQgZGVjaWRlIHdoaWNoIHRvIHJlY29tbWVuZC4NCg0KIyMjIFVzZXIgUHJvZmlsZQ0KDQpgYGB7cn0NCnVzZXIgPC0gaXRlbVssMTo1XSAlKiUgdChyYXRpbmcpIC8gcm93U3VtcyhpdGVtKSAjIyBUaGlzIGlzIHRoZSBGT1JNVUxBIGluIHNsaWRlcw0KDQp1c2VyICU+JSANCiAga2JsKCkgJT4lDQogIGthYmxlX3N0eWxpbmcoKQ0KYGBgDQoNCiMgSUkuIENPTExBQk9SQVRJVkUgRklMVEVSSU5HDQoNCkZyb20gbGVjdHVyZSB3ZSBnYXZlIGFuIGV4YW1wbGUgb2YgNyB1c2VycyBhbmQgNiBpdGVtcy4gV2UgYXJlIHRyeWluZyB0byBwcmVkaWN0ICp3aGV0aGVyIHRvIHJlY29tbWVuZCBQcmVkYXRvciBvciBOb3R0aW5nIEhpbGwgdG8gQWRhbSBiYXNlZCBvbiBoaXMgc2ltaWxhcml0eSB3aXRoIG90aGVycyouDQoNCmBgYHtyfQ0KDQpyb3dfbmFtZXM8LWMoIkEiLCAiQiIsICJDIiwgIkQiLCAiRSIsICJGIiwgIkciKQ0KY29sX25hbWVzPC1jKCJQVyIsICJUUiIsICJFQiIsICJUMiIsICJQIiwgIk5IIikNCg0KdXRpbCA8LSBtYXRyaXgoYygyLDUsNCwyLE5BLCBOQSwNCiAgICAgICAgICAgICAgNSwxLDIsTkEsMSxOQSwNCiAgICAgICAgICAgICAgNSw1LDUsNSw1LDUsDQogICAgICAgICAgICAgIDIsNSxOQSwzLE5BLE5BLA0KICAgICAgICAgICAgICA1LDQsNSwzLE5BLDUsDQogICAgICAgICAgICAgIDEsNSxOQSxOQSxOQSwxLA0KICAgICAgICAgICAgICAyLE5BLDUsTkEsNSxOQSksYnlyb3cgPSBUUlVFLCBucm93ID0gNywgbmNvbCA9IDYsIGRpbW5hbWVzPWxpc3Qocm93X25hbWVzLGNvbF9uYW1lcykpDQp1dGlsICU+JSANCiAga2JsKCkgJT4lDQogIGthYmxlX3N0eWxpbmcoKQ0KDQpgYGANCg0KIyMgVXNlciBCYXNlZA0KDQpMZXQncyB0YWtlIGEgbG9vayBhdCB1c2VyLWJhc2VkIGNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nLiBXZSdsbCB1c2UgKipzaW1wbGUgY29ycmVsYXRpb25zKiogYmV0d2VlbiB1c2VycyB0byBzZWUgd2hvIGlzIG1vcmUgc2ltaWxhciB0byBBLg0KDQpgYGB7cn0NCmNvcih0KHV0aWwpLCB1c2U9InBhaXJ3aXNlLmNvbXBsZXRlLm9icyIpICU+JSANCiAga2JsKCkgJT4lDQogIGthYmxlX3N0eWxpbmcoKQ0KYGBgDQoNCkZyb20gdGhpcyB3ZSBzZWUgdGhhdCBvbmx5IEIsIEYgYW5kIEcgYXJlIHJlbGV2YW50OiBXZSBmb2N1cyBvbmx5IG9uIHRoZSAzIG1vc3Qgc2ltaWxhciBjdXN0b21lcnMsIEIgRiBhbmQgRy4NCg0KYGBge3J9DQptIDwtIGNvcih0KHV0aWwpLCB1c2U9InBhaXJ3aXNlLmNvbXBsZXRlLm9icyIpICMgcmUtcnVuIHRoZSBjb3JyZWxhdGlvbiBtYXRyaXgNCg0KIyBUaGUgcmVsZXZhbnQgcm93DQptW3Jvdz1jKCJCIiwiRiIsIkciKSwgY29sPWMoIkEiKV0gJT4lIA0KICBrYmwoKSAlPiUNCiAga2FibGVfc3R5bGluZygpDQpgYGANCg0KV2Ugbm9ybWFsaXplIHRoZSByYXRpbmdzIGFuZCBtdWx0aXBseSB0aGUgY29ycmVsYXRpb25zIGJ5IHRoZWlyIHJhdGluZ3Mgb2YgdGhlIG1vdmllcyBpbiBxdWVzdGlvbiwgUCBhbmQgTkguIFRoZW4gd2UgYXZlcmFnZSB0byBnZXQgdGhlIHByZWRpY3RlZCByYXRpbmdzIG9mIEFkYW0uDQoNCmBgYHtyfQ0KdXRpbF9uIDwtIHV0aWwgLSByb3dNZWFucyh1dGlsLCBuYS5ybT1UUlVFKSAjbm9ybWFsaXplDQoNCiMjIE11bHRpcGx5IHRoZSBjb3JyZWxhdGlvbnMgYnkgdGhlaXIgcmF0aW5ncyBvZiB0aGUgbW92aWVzIGluIHF1ZXN0aW9uLCBQIGFuZCBODQpwcmVkbSA8LSBtW3Jvdz1jKCJCIiwiRiIsIkciKSxjb2w9YygiQSIpXSp1dGlsX25bcm93PWMoIkIiLCJGIiwiRyIpLCBjb2w9YygiUCIsIk5IIildDQojIyBUYWtlIHRoZSBhdmVyYWdlDQpwcmVkIDwtIGNvbE1lYW5zKHByZWRtLCBuYS5ybT1UUlVFKQ0KcHJlZCAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCmBgYA0KDQpBZGFtJ3MgcmF0aW5ncyB3b3VsZCBiZSAxLjEyMiBoaWdoZXIgdGhhbiBhdmVyYWdlIGZvciBQIGFuZCAtMS4zMzMgZm9yIE5ILg0KDQojIyBJdGVtIEJhc2VkDQoNCk5vdyBmb3IgdGhlIGl0ZW0gYmFzZWQgZmlsdGVyaW5nLiBXZSBkbyB0aGUgY29ycmVsYXRpb25zIGFjcm9zcyBjb2x1bW5zIGluc3RlYWQgb2Ygcm93cy4NCg0KYGBge3IsIHdhcm5pbmc9RkFMU0V9DQptIDwtIGNvcih1dGlsLCB1c2U9InBhaXJ3aXNlLmNvbXBsZXRlLm9icyIpDQpgYGANCg0KRm9jdXMgb24gbGFzdCB0d28gY29sdW1ucy4gV2UgZm9jdXMgb24gdGhlIG1vdmllcyB0aGF0IGhhdmUgZWl0aGVyIHBlcmZlY3QgcG9zaXRpdmUgb3IgbmVnYXRpdmUgY29ycmVsYXRpb24uIEZvciBQIHRoYXQgaXMgVFIgYW5kIEVCLCBmb3IgTkggdGhhdCBpcyBQVy4NCg0KYGBge3J9DQptIDwtIG1bcm93PWMoIlBXIiwgIlRSIiwiRUIiKSwgY29sPWMoIlAiLCAiTkgiKV0NCg0KIyBtYWtlIE5BIGFueXRoaW5nIGxlc3MgdGhhbiAxDQptW2FicyhtKTwxXSA8LSBOQQ0KbSAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCmBgYA0KDQpUaGUgcHJlZGljdGlvbiBpcyB0aGUgcHJvZHVjdCBvZiB0aGUgY29ycmVsYXRpb24gYmV0d2VlbiB0aGUgdGFyZ2V0IG1vdmllIGFuZCB0aGUgb3RoZXIgbW92aWVzIGFuZCBBZGFtJ3Mgbm9ybWFsaXplZCByZXZpZXdzIGZvciB0aGUgb3RoZXIgbW92aWVzOg0KDQpgYGB7cn0NCnByZWRtPC1tKnV0aWxfbltyb3c9YygiQSIpLGNvbD1jKCJQVyIsICJUUiIsIkVCIildDQpwcmVkbSAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCg0KcHJlZDwtY29sTWVhbnMocHJlZG0sIG5hLnJtID0gVFJVRSkNCnByZWQgJT4lIA0KICBrYmwoKSAlPiUNCiAga2FibGVfc3R5bGluZygpDQpgYGANCg0KQWRhbSdzIHJhdGluZ3Mgd291bGQgYmUgMS4yNSBoaWdoZXIgdGhhbiBhdmVyYWdlIGZvciBQIGFuZCAtMS4yNSB0aGFuIGF2ZXJhZ2UgZm9yIE5ILg0KDQojIElJSS4gUkVBTCBEQVRBIEVYQU1QTEVTDQoNCiMjIDAuIERhdGENCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQpkYXRhKCJNb3ZpZUxlbnNlIikNCk1vdmllTGVuc2UNCiNnZXRSYXRpbmdNYXRyaXgoTW92aWVMZW5zZSlbMToxMCwxOjVdDQphcyhNb3ZpZUxlbnNlLCAibWF0cml4IilbMToxMCwgMTo1XQ0KYGBgDQoNCmBgYHtyLCBjYWNoZT1UUlVFfQ0KdGVzdCA8LSBhcyhNb3ZpZUxlbnNlLCAibWF0cml4IilbMToxMCxdDQppbWFnZShNb3ZpZUxlbnNlKQ0KYGBgDQoNCmBgYHtyfQ0KY291bnQgPC0gY29sQ291bnRzKE1vdmllTGVuc2UpDQpoZWFkKHNvcnQoY291bnQsIGRlY3JlYXNpbmcgPSBUUlVFKSkNCg0KaGlzdChjb2xDb3VudHMoTW92aWVMZW5zZSksIHhsYWI9Im51bWJlciBvZiByZXZpZXdzIiwgbWFpbiA9ICJudW1iZXIgb2YgcmV2aWV3cyBwZXIgbW92aWUiKQ0KaGlzdChjb2xNZWFucyhNb3ZpZUxlbnNlKSwgeGxhYj0iYXZlcmFnZSBtb3ZpZSByYXRpbmdzIiwgbWFpbj0iIiwgYnJlYWtzPTUwKQ0KDQpgYGANCg0KIyMgMS4gQ29udGVudCBGaWx0ZXJpbmcNCg0KIyMjIEEuIEJ1aWxkIEl0ZW0gUHJvZmlsZQ0KDQpUaGVyZSBhcmUgYSBidW5jaCBvZiBtZXRhLWNoYXJhY3RlcmlzdGljcyBhdmFpbGFibGUuIFdlJ2xsIHVzZSB0aGUgZ2VucmVzOiBmcm9tIHVua25vd24gdG8gV2VzdGVybiBhcyBvdXIgaXRlbSBjaGFyYWN0ZXJpc3RpY3MuDQoNCmBgYHtyfQ0KaGVhZChNb3ZpZUxlbnNlTWV0YSkNCml0ZW0gPC0gYXMubWF0cml4KHN1YnNldChNb3ZpZUxlbnNlTWV0YSwgc2VsZWN0ID0gLWModGl0bGUsIHllYXIsIHVybCkpKQ0KYGBgDQoNCiMjIyBCLiBCdWlsZCBVc2VyIFByb2ZpbGUNCg0KV2UnbGwgdGFrZSB1c2VyIDEgYXMgb3VyICJBZGFtIiwgb3VyIHVzZXIgb24gd2hpY2ggdG8gYnVpbGQgb3VyIGNvbnRlbnQgZmlsdGVyaW5nIHN5c3RlbS4gV2Ugbm9ybWFsaXplIGhpcyByYXRpbmdzIGJ5IHN1YnRyYWN0aW5nIG9mZiB0aGUgbWVhbi4gV2UgY3JlYXRlIGFuIGluZGV4LCBub25fbWlzcyBvZiB0aGUgcmF0aW5ncyBoZSBnaXZlcy4NCg0KYGBge3J9DQpyYXRpbmcgPC0gYXMoTW92aWVMZW5zZSwgIm1hdHJpeCIpWzEsXQ0KDQpyYXRpbmdfbSA8LSByYXRpbmctbWVhbihyYXRpbmcsbmEucm09VFJVRSkgIyMgbm9ybWFsaXplDQoNCm5vbl9taXNzIDwtICFpcy5uYShyYXRpbmdfbSkNCm1pc3MgPC0gaXMubmEocmF0aW5nX20pDQpgYGANCg0KV2UgY2FsY3VsYXRlIGhpcyB1c2VyIHByb2ZpbGUgdXNpbmcgdGhlICoqZm9ybXVsYSoqIChzbGlkZXMpLiBPbmx5IGRpZmZlcmVuY2UgaXMgdGhhdCB0aGUgaXRlbSBtYXRyaXggaXMgdGhlIG9wcG9zaXRlIGZyb20gdGhlIGFib3ZlIGV4YW1wbGU6ICptb3ZpZXMgYXJlIHJvd3MgYW5kIGF0dHJpYnV0ZXMgYXJlIGNvbHVtbnMqLg0KDQpTbyB3ZSBjaGFuZ2UgdGhlIG1hdHJpeCBtdWx0aXBsaWNhdGlvbjogKnRyYW5zcG9zZSBpdGVtIG1hdHJpeCogYW5kICp0YWtlIGNvbHVtbiogc3VtcyByYXRoZXIgdGhhbiByb3cgc3Vtcy4NCg0KYGBge3J9DQp1c2VyIDwtICh0KGl0ZW1bbm9uX21pc3MsXSkgJSolIHJhdGluZ19tW25vbl9taXNzXSkgLyBjb2xTdW1zKGl0ZW1bbm9uX21pc3MsIF0pDQp1c2VyDQpgYGANCg0KIyMjIEMuIFNpbWlsYXJpdHkgUHJlZGljdGlvbg0KDQpXZSB0YWtlIGFsbCBvZiB0aGUgbW92aWVzIGhlL3NoZSBoYXMgbm90IHNlZW4sIGFuZCBtYWtlIG91ciBjb3NpbmUgc2ltaWxhcml0eSBwcmVkaWN0aW9ucyBvbiB0aGVtLg0KDQpgYGB7cn0NCm5hbWVzIDwtIGFzLm1hdHJpeChzdWJzZXQoTW92aWVMZW5zZU1ldGEsIHNlbGVjdCA9IGModGl0bGUpKSkNCm5ld19pdGVtIDwtIGl0ZW1bbWlzcyxdDQpuZXdfbmFtZXMgPC0gbmFtZXNbbWlzcyxdDQpgYGANCg0KV2UgYXBwbHkgdGhlIGZvcm11bGE6DQoNCmBgYHtyfQ0KQ1MgPSAobmV3X2l0ZW0pICUqJSB1c2VyIC8gKHNxcnQocm93U3VtcyhuZXdfaXRlbV4yKSkqc3FydChzdW0odXNlcl4yKSkpDQpoaXN0KENTLCBtYWluID0gImhpc3RvZ3JhbSBvZiBjb3NpbmUgc2ltaWxhcml0eSB3aXRoIHVuc2VlbiBtb3ZpZXMiLCB4bGFiPSJDb3NpbmUgU2ltaWxhcml0eSIpDQoNCmBgYA0KDQpUaGUgdG9wIDYgbW92aWVzIHByZWRpY3RlZCAob2YgdGhlIDEzOTMgbm90IHJhdGVkIG1vdmllcykgYXJlOg0KDQpgYGB7cn0NCm5ld19uYW1lc1toZWFkKG9yZGVyKENTLCBkZWNyZWFzaW5nID0gVFJVRSkpXQ0KYGBgDQoNCiMjIDIuIE5vbi1QZXJzb25hbGl6ZWQgUmVjb21tZW5kYXRpb25zOiBQb3B1bGFyaXR5DQoNClBvcHVsYXIgbm9ybWFsaXplcyB0aGUgcmF0aW5ncyBieSB1c2VyLCBhbmQgdGFrZXMgdGhlIGF2ZXJhZ2UgYWNyb3NzIHVzZXJzLiBJdCBkb2Vzbid0IHJlY29tbWVuZCBzb21ldGhpbmcgdG8gc29tZW9uZSB3aG8gaGFzIGFscmVhZHkgcmF0ZWQgaXQuIEJ1dCBpdCBzdGFydHMgYXQgdGhlIHRvcCBvZiB0aGUgbGlzdCBhbmQgZ29lcyBkb3duLg0KDQpgYGB7cn0NCnJvd19uYW1lczwtYygiQSIsICJCIiwgIkMiLCAiRCIsICJFIiwgIkYiLCAiRyIpDQpjb2xfbmFtZXM8LWMoIlBXIiwgIlRSIiwgIkVCIiwgIlQyIiwgIlAiLCAiTkgiKQ0KDQp1dGlsIDwtIG1hdHJpeChjKDIsNSw0LDIsTkEsIE5BLA0KICAgICAgICAgICAgICA1LDEsMixOQSwxLE5BLA0KICAgICAgICAgICAgICA1LDUsNSw1LDUsNSwNCiAgICAgICAgICAgICAgMiw1LE5BLDMsTkEsTkEsDQogICAgICAgICAgICAgIDUsNCw1LDMsTkEsNSwNCiAgICAgICAgICAgICAgMSw1LE5BLE5BLE5BLDEsDQogICAgICAgICAgICAgIDIsTkEsNSxOQSw1LE5BKSxieXJvdyA9IFRSVUUsIG5yb3cgPSA3LCBuY29sID0gNiwgZGltbmFtZXM9bGlzdChyb3dfbmFtZXMsY29sX25hbWVzKSkNCnV0aWwgJT4lIA0KICBrYmwoKSAlPiUNCiAga2FibGVfc3R5bGluZygpDQpgYGANCg0KYGBge3J9DQp1dGlsX24gPC11dGlsLXJvd01lYW5zKHV0aWwsIG5hLnJtPVRSVUUpDQpgYGANCg0KSW4gb3VyIGV4YW1wbGUgYWJvdmUgaW4gdGhlIGNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nIHBhcnQsIGl0IHdvdWxkIG1ha2UgVFIgdGhlIGZpcnN0LCBFQiB0aGUgc2Vjb25kLCBldGMuIElmIHNvbWVvbmUgaGFkIGFscmVhZHkgd2F0Y2hlZCBUUiwgdGhlIGZpcnN0IHJlY29tbWVuZGF0aW9uIHdvdWxkIGJlIEVCLCB0aGVuIFAsIGV0Yy4NCg0KYGBge3J9DQpjb2xNZWFucyh1dGlsX24sbmEucm0gPSBUUlVFKSAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCmBgYA0KDQpgYGB7cn0NCnRlc3Q8LSBhcyh1dGlsLCAicmVhbFJhdGluZ01hdHJpeCIpDQp0ZXN0X3JlY29tPC1SZWNvbW1lbmRlcih0ZXN0LCBtZXRob2QgPSAiUE9QVUxBUiIpDQp0ZXN0X3JlY29tQG1vZGVsJHRvcE5AaXRlbXMNCmBgYA0KDQpgYGB7cn0NCnRlc3RfcHJlZDwtcHJlZGljdCh0ZXN0X3JlY29tLCB0ZXN0WzEsXSx0eXBlPSJyYXRpbmdzIikNCg0KYXModGVzdF9wcmVkLCJtYXRyaXgiKSAlPiUgDQogIGtibCgpICU+JQ0KICBrYWJsZV9zdHlsaW5nKCkNCmBgYA0KDQpBZGFtJ3MgYXZlcmFnZSByZXZpZXcgaXMgMy4yNS4gVGhlIGF2ZXJhZ2UgcmF0aW5nIG9mIFAgaXMgLTAuMDgzIGNvbXBhcmVkIHRvIHRoZSBhdmVyYWdlLiBIZW5jZSB0aGUgcHJlZGljdGlvbiBvZiB0aGUgcG9wdWxhciBtb2RlbCBpcyB0aGVzZSB0d28gcXVhbnRpdGllcyBhZGRlZCwgMy4xNjcuDQoNCiMjIDMuIENvbGxhYm9yYXRpdmUgRmlsdGVyaW5nDQoNCiMjIyAwLiBNb3ZpZUxlbnNlICJNZXRob2QNCg0KYGBge3J9DQpzZXQuc2VlZCgxOTEwMykNCmVzIDwtIGV2YWx1YXRpb25TY2hlbWUoTW92aWVMZW5zZSwgDQogIG1ldGhvZD0ic3BsaXQiLCB0cmFpbj0wLjksIGdpdmVuPTE1KQ0KDQplcw0KYGBgDQoNCmBgYHtyfQ0KdHJhaW4gPC0gZ2V0RGF0YShlcywgInRyYWluIik7IHRyYWluDQp0ZXN0X2tub3duIDwtIGdldERhdGEoZXMsICJrbm93biIpOyB0ZXN0X2tub3duDQp0ZXN0X3Vua25vd24gPC0gZ2V0RGF0YShlcywgInVua25vd24iKTsgdGVzdF91bmtub3duDQpgYGANCg0KYGBge3J9DQpwb3B1bGFyIDwtUmVjb21tZW5kZXIodHJhaW4sICJQT1BVTEFSIikNCiMjIGNyZWF0ZSBwcmVkaWN0aW9ucyBmb3IgdGhlIHRlc3QgdXNlcnMgdXNpbmcga25vd24gcmF0aW5ncw0KcHJlZF9wb3AgPC0gcHJlZGljdChwb3B1bGFyLCB0ZXN0X2tub3duLCB0eXBlPSJyYXRpbmdzIik7IHByZWRfcG9wDQpgYGANCg0KYGBge3J9DQojIyBldmFsdWF0ZSByZWNvbW1lbmRhdGlvbnMgb24gInVua25vd24iIHJhdGluZ3MNCmFjY19wb3AgPC0gY2FsY1ByZWRpY3Rpb25BY2N1cmFjeShwcmVkX3BvcCwgdGVzdF91bmtub3duKTsNCmFzKGFjY19wb3AsIm1hdHJpeCIpICU+JSANCiAga2JsKCkgJT4lDQogIGthYmxlX3N0eWxpbmcoKQ0KYGBgDQoNCmBgYHtyfQ0KYXModGVzdF91bmtub3duLCAibWF0cml4IilbMTo4LDE6NV0NCmFzKHByZWRfcG9wLCAibWF0cml4IilbMTo4LDE6NV0NCmBgYA0KDQojIyMgQS4gVXNlci1CYXNlZA0KDQpOb3cgd2UnbGwgdXNlIHVzZXItYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXJpbmcuIFdlJ2xsIHVzZSAocGVhcnNvbikgY29ycmVsYXRpb24gdG8gZGV0ZXJtaW5lIHRoZSBzaW1pbGFyaXR5IGFjcm9zcyB1c2Vycy4NCg0KQW5kIHdlJ2xsIHVzZSB0aGUgMzAgbW9zdCBzaW1pbGFyIHVzZXJzIGluIG1ha2luZyBvdXIgcmVjb21tZW5kYXRpb24uDQoNCmBgYHtyfQ0KVUJDRiA8LSBSZWNvbW1lbmRlcih0cmFpbiwgIlVCQ0YiLA0KICAgICAgICAgICAgICAgICAgICAgICAgcGFyYW09bGlzdChtZXRob2Q9InBlYXJzb24iLG5uPTMwKSkNCg0KIyMgY3JlYXRlIHByZWRpY3Rpb25zIGZvciB0aGUgdGVzdCB1c2VycyB1c2luZyBrbm93biByYXRpbmdzDQpwcmVkX3ViIDwtIHByZWRpY3QoVUJDRiwgdGVzdF9rbm93biwgdHlwZT0icmF0aW5ncyIpOyBwcmVkX3ViDQpgYGANCg0KYGBge3J9DQojIyBldmFsdWF0ZSByZWNvbW1lbmRhdGlvbnMgb24gInVua25vd24iIHJhdGluZ3MNCmFjY191YiA8LSBjYWxjUHJlZGljdGlvbkFjY3VyYWN5KHByZWRfdWIsIHRlc3RfdW5rbm93bik7DQphY2MgPC0gcmJpbmQoUE9QPWFjY19wb3AsIFVCQ0YgPSBhY2NfdWIpOyBhY2MNCmBgYA0KDQpgYGB7cn0NCmFzKHRlc3RfdW5rbm93biwgIm1hdHJpeCIpWzE6OCwxOjVdDQphcyhwcmVkX3ViLCAibWF0cml4IilbMTo4LDE6NV0NCmBgYA0KDQpVQkNGIGhhcyBoaWdoZXIgZXJyb3IgbWV0cmljcyB0aGFuIHBvcHVsYXJpdHksIGluZGljYXRpbmcgd29yc2UgZml0Lg0KDQojIyMgQi4gSXRlbS1CYXNlZA0KDQpIZXJlIHdlIHVzZSBpdGVtLWJhc2VkIGNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nLCB1c2luZyBwZWFzb24gY29ycmVsYXRpb24gdG8gZGV0ZXJtaW5lIHNpbWlsYXJpdHkgYWNyb3NzIGl0ZW1zLiBBbmQgd2UgdXNlIHRoZSAzMCBtb3N0IHNpbWlsaWFyIGl0ZW1zLg0KDQpgYGB7cn0NCklCQ0YgPC0gUmVjb21tZW5kZXIodHJhaW4sICJJQkNGIiwNCiAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFtPWxpc3QobWV0aG9kPSJwZWFyc29uIixrPTMwKSkNCg0KcHJlZF9pYiA8LSBwcmVkaWN0KElCQ0YsIHRlc3Rfa25vd24sIHR5cGU9InJhdGluZ3MiKQ0KDQphY2NfaWIgPC0gY2FsY1ByZWRpY3Rpb25BY2N1cmFjeShwcmVkX2liLCB0ZXN0X3Vua25vd24pIA0KDQphY2MgPC0gcmJpbmQoUE9QPWFjY19wb3AsIFVCQ0YgPSBhY2NfdWIsIElCQ0YgPSBhY2NfaWIpOyBhY2MNCmBgYA0KDQpOb3RlIHRoZSBlcnJvciBtZXRyaWMgaXMgeWV0IHdvcnNlIGZvciBJQkNGLg0K