Problem definition

The data set describes customer orders over time. The goal of the experiment is to predict which products users will buy next, based on their purchase history. For convenience, the original kaggle problem has been slightly reformulated: we should only consider <userid, productid> pairs and discard any other associated features like day of week and hour of day of the purchase. Let’s get started!

The two main packages that we will be using are “recommenderlab” and “recosystem”. These packages provide different algorithms for collaboration filtering (matrix completion). More info can be found here:

  1. recommenderlab package
  2. recosystem package


Load/install libraries

Let’s load the required libraries and source methods first.

Note: We will use the latest (beta) version of the “recommenderlab” package. You should directly install it from github and should NOT use “install.package” (see commented code below)

require(dplyr)
require(data.table)

# library(devtools)
# 
# if("recommenderlab" %in% rownames(installed.packages()) == FALSE){
#   install_github("mhahsler/recommenderlab")
# }

require(recommenderlab)
require(recosystem)
require(ggplot2)
require(grid)
require(gridExtra)

source("crossvalidateALS.R")
source("partitionData.R")
source("crossvalidateUBCF.R")
source("evaluateModel2.R")
source("prepareTrainTestData.R")
source("padTestData.R")


Read data

Next, let’s read the data files (stored locally in “instacart_2017_05_01” directory)

aisles = fread('instacart_2017_05_01/aisles.csv')             # 134 aisles
departments = fread('instacart_2017_05_01/departments.csv')   # 21 departments
products = fread('instacart_2017_05_01/products.csv')         # ~50K products
order_products_prior = fread('instacart_2017_05_01/order_products__prior.csv',key=c("order_id"))    # ~32M individual products ordered
order_products_train = fread('instacart_2017_05_01/order_products__train.csv',key=c("order_id"))    # ~1.3M individual products ordered (ignore)
orders = fread('instacart_2017_05_01/orders.csv',key=c("order_id"))


Data preprocessing

  • We will consider only the orders data marked “prior”.
  • The data set is normalized, so to obtain <userid, productid> pairs, we need to join “orders_prior” and “order_products_prior” tables on the key “order_id”.
  • We will then sort the resulting table (“order_all”) on “user_id” and “product_id” keys
  • From this, we’ll obtain “data_ind” which is a list of individual <user_id, product_id> pairs
  • From “data_ind”, we can accumulate similar <user_id, product_id> pairs and form “data” table which contains <user_id, product_id, count> triples
orders_prior = subset(orders,eval_set=="prior")         # ~3.2M "prior" orders
orders_all = merge(orders_prior,order_products_prior, by="order_id")

orders_all = orders_all[order(orders_all$user_id,orders_all$product_id),]

data_ind = orders_all[,c("user_id", "product_id")]

setkey(data_ind,user_id,product_id)

data = aggregate(cbind(count = product_id) ~ product_id+user_id, data=data_ind, FUN = length)
data = setDT(data[,c(2,1,3)])


Generating train/test data

The data set is too big to work with on a standalone computer, so we are going to downsample it; although ideally, we can hope to get best performance using full data. Let’s see how the distribution of total product purchases across all users looks like

product_counts = aggregate(cbind(total = count) ~ product_id, data=data[,2:3], FUN = sum)
ggplot(product_counts, aes(total)) + geom_histogram(binwidth = 1000) + xlab("number of times bought")

As expected, this looks highly non-uniform. Let’s look at two different ranges for better understanding: products that are bought less than 2000 times and ones bought between 2000 to 20000 times

p1 = ggplot(subset(product_counts,total<2000), aes(total)) + geom_histogram(binwidth = 10) + xlab("number of times bought")
p2 = ggplot(subset(product_counts,total>2000 & total < 20000), aes(total)) + geom_histogram(binwidth = 100) + xlab("number of times bought") + xlim(1000,20000)
grid.arrange(p1, p2, ncol = 2)

Similarly, let’s plot the number of frequency of different users in the data:

user_counts = aggregate(cbind(total = count) ~ user_id, data=data[,c(1:3)], FUN = sum)
ggplot(user_counts, aes(total)) + geom_histogram(binwidth = 10) + xlab("Total number of products bought by a user")

As we can see that the number of products and users decrease sharply with their frequency. In other words:

  • Most products were bought rarely, while only a very few products were frequently bought
  • Most users bought very few products, while very few users bought a lot of products

Given this, there are different ways in which we can down-sample the data:

  • Scheme 1 (Uniform sampling): Randomly pick P products and U users
    • Pros: Data distributions of popular vs. rarely-bought products and frequent vs. infrequent users are preserved
    • Cons: With small P and U, we might end up with mostly rarely bought products and infrequent users, because those are the ones with vast majority.
  • Scheme 2 (Weighted sampling): Pick P products and U users based on their frequency of purchase
    • Pros: We will get more frequent users and popular products, so the data matrix will be more dense, thus increasing the predictive power of the algorithm
    • Cons: Original data distributions are not preserved. So performance might be poor rarely-bought products and infrequent users
  • Scheme 3 (Hybrid sampling): Pick P products and U users based on some combination of Scheme 1 and 2 above. For example, we can divide the products and users into N percentiles based on their frequency and then uniformly sample from each bin
    • Pros: We will get a good mix of all types of users and products, so overall predictive power should be better
    • Cons: Original data distributions are not preserved

For now, we’ll go with Scheme 1, in favor of preserving the original data distribution with P=5000 and U=15000

data_orig = data # save the original data

users = unique(data$user_id)
user_samples = users[sample(length(unique(data$user_id)), 15000, replace = FALSE)]
data = subset(data,user_id %in% user_samples)

products = unique(data$product_id)
product_samples = products[sample(length(unique(data$product_id)), 5000, replace = FALSE)]
data = subset(data,product_id %in% product_samples)
  • Now, partition the data into train/test (70/30) making sure that the following conditions are met:
    1. All product id’s and user id’s are there in both train and test sets
    2. A pair <userid, productid> can either appear in train or test set, but not both
  • In “partitionData” method, we partition the data in two steps:
    1. Randomly partition “data” table into “train” and “test” tables as per the required split ratio
    2. Remove users/products that are there in one set and not in another
    Note: This is not the best way to split the data, and incurs some loss. A better approach would be to partition it based on the joint distribution of users and products in “data”
data_parts = partitionData(data = data, fracData = 1, fracTrain = 0.7)
train = data_parts$train
test = data_parts$test

Ok, now we should have a train/test split where both conditions 1 & 2 are satisfied. Let’s check split ratio again

cat("Train % = ", nrow(train)/(nrow(train) + nrow(test))*100, "and test % = ", nrow(test)/(nrow(train) + nrow(test))*100, "\n")
Train % =  67.99588 and test % =  32.00412 
cat("nrows train = ", nrow(train), "\n")
nrows train =  79307 
cat("nrows test = ", nrow(test), "\n")
nrows test =  37328 
cat("Number of unique user_id's = ", length(unique(train$user_id)), "\n")
Number of unique user_id's =  11263 
cat("Number of unique product_id's = ", length(unique(train$product_id)), "\n")
Number of unique product_id's =  3138 

Also, let’s quickly compare the sparsity of the train/test data w.r.t. original data

cat("Sparsity in original data = ", 100 - nrow(data_orig)/(as.double(length(unique(data_orig$product_id)))*as.double(length(unique(data_orig$user_id))))*100, "%\n")
Sparsity in original data =  99.87009 %
cat("Sparsity in train data = ", 100 - nrow(train)/(as.double(length(unique(train$product_id)))*as.double(length(unique(train$user_id))))*100, "%\n")
Sparsity in train data =  99.77561 %
cat("Sparsity in test data = ", 100 - nrow(test)/(as.double(length(unique(test$product_id)))*as.double(length(unique(test$user_id))))*100, "%\n")
Sparsity in test data =  99.89438 %

So it is roughly the same, which is what we expect with uniform sampling approach


Exploring Weighted Alternating Least Square approach (via “recommenderlab” package)

The “recommenderlab” package implements the ALS (alternating least squares) optimization approach described in Hu et. al.’s paper

Note 1: The package has a routine to tune different parameters: the regularization parameter, lambda and length of the latent factors, d. But the tuning is performed while optimizing for MSE (mean squared error), MAE (mean absolute error) or RMSE (root mean squared error). However, since we are interested in minimizing the MPR (mean percentile ranking), we’ll use our own method for tuning these parameters

Note 2: Ideally, for cross-validation, we should partition the training data into f folds and use leave-one-fold-out technique to tune the parameters. However, in favor of tuning the parameters faster, we will resample the “train” data multiple times to generate training/validation sets and simply average the MPR over different runs (folds) for each set of parameters

source("evaluateModel.R")
source("crossvalidateALS.R")
lambda = c(0.01,1,10)
nFactors = c(10,100,200)
alpha = c(1, 10, 100)
ranks_ALS = crossvalidateALS(data = train, lambda = lambda, nFactors = nFactors, alpha = alpha, fracData = 0.2, cvFolds = 4, nIter = 10, fracTrain = 0.7)
ranks_ALS
, , 1

         [,1]     [,2]     [,3]
[1,] 45.45103 43.43268 43.18959
[2,] 41.96728 41.95470 42.79556
[3,] 34.40228 34.87970 36.81492

, , 2

         [,1]     [,2]     [,3]
[1,] 42.14331 48.82323 48.36222
[2,] 39.80476 37.61222 41.14862
[3,] 29.15106 32.77523 32.15245

, , 3

         [,1]     [,2]     [,3]
[1,] 36.41889 48.94756 43.62423
[2,] 36.68782 42.87251 41.23936
[3,] 36.58844 32.76308 35.26052
lambda_best = lambda[which(ranks_ALS == min(ranks_ALS), arr.ind = TRUE)[1]]
nFactors_best = nFactors[which(ranks_ALS == min(ranks_ALS), arr.ind = TRUE)[2]]
alpha_best = nFactors[which(ranks_ALS == min(ranks_ALS), arr.ind = TRUE)[3]]
cat("best lambda value =", lambda_best, "and best d value =", nFactors_best, "and best alpha value =", alpha_best, "\n")
best lambda value = 10 and best d value = 10 and best alpha value = 100 

Now that we have the optimal parameters, lets train on full training data using the optimal parameter values computed above and compute/evaluate the predictions. Note that, since we downsampled the data, padding with 100 random samples instead of 1000 should be sufficient

R = as(train, "realRatingMatrix")
r = Recommender(R, method = "ALS_implicit", parameter = list(n_factors=nFactors_best, lambda=lambda_best, alpha=alpha_best, n_iterations=20, seed = NULL, verbose = TRUE)) 
reco = predict(r, R, type = "ratings")
rank_final_recolab = evaluateModel(reco = reco, test = test, pad = 100)
cat("Mean MPR across all users = ", mean(rank_final_recolab),"\n")
Mean MPR across all users =  13.49061 
cat("Median MPR across all users = ", median(rank_final_recolab),"\n")
Median MPR across all users =  7.311321 

Let’s also look at the full histogram of individual PR’s

ggplot(as.data.frame(rank_final_recolab), aes(rank_final_recolab, fill= rank_final_recolab > 50)) + geom_histogram(binwidth = 1) + xlab("Individual Percentile Ranking") + ylab("Number of users")

cat("Number of users with better than random predictions =", length(subset(rank_final_recolab,rank_final_recolab<50)), "\n")
Number of users with better than random predictions = 10701 
cat("Number of users with worse than random predictions =", length(subset(rank_final_recolab,rank_final_recolab>=50)), "\n")
Number of users with worse than random predictions = 562 

Let’s also look at the scatter plot between individual user PRs and their frequency of purchase

user_counts_test = aggregate(cbind(total = count) ~ user_id, data=test[,c(1:3)], FUN = sum)
user_counts_test = user_counts_test[order(user_counts_test$user_id),]
pred_ALS = getData.frame(reco, decode = TRUE, ratings = TRUE)
users_ALS = data.frame(unique(pred_ALS$user),rank_final_recolab)
user_counts_test$ranks_ALS = users_ALS$rank_final_recolab
ggplot(user_counts_test) + geom_point(aes(x=total, y=ranks_ALS, color = ranks_ALS > 50)) + xlab("Total purchases") + ylab("Individual Percentile Ranking")

Looks like the learning is quite uniform across all types of users (frequent vs infrequent buyers). Let’s also look at the percentages of users who bought < 25 products

cat('% of users who bought < 25 products (overall) =', nrow(subset(user_counts_test,total<25))/nrow(user_counts_test)*100, "%\n")
% of users who bought < 25 products (overall) = 92.06251 %
cat('% of users who bought < 25 products (with PR > 50%) =', nrow(subset(user_counts_test,total<25 & ranks_ALS > 50))/nrow(subset(user_counts_test, ranks_ALS > 50))*100, "%\n")
% of users who bought < 25 products (with PR > 50%) = 91.62113 %

This verifies the fact that the learning is quite uniform across different types of users


Exploring matrix factorization (via “recosystem” package)

Let’s explore the “recosystem” library. This package has a much more efficient implementation using SGD and also supports parallel/cluster computation; although for the more traditional matrix factorization, which does not modify the cost function for implicit feedback data

Nonetheless, I thought it would be interesting to compare it with the ALS method. In addition to tuning the number of factors, it provides hooks to tune the learning rate (step size in SGD) and two separate regularization coefficients for user and product factors for both l1 and l2 optimizers.

Note: Like earlier, ideally, we should optimize for MPR while doing parameter tuning. But, we’ll ignore that for now and optimize the default L2-norm

r2 = Reco()

opts = r2$tune(data_memory(train$user_id,train$product_id,train$count), opts = list(dim = c(10,100,200), lrate = c(0.1, 1, 10), costp_l2 = c(0.1, 1, 10), costq_l2 = c(0.1, 1, 10), nthread = 4, niter = 20))
opts$min
$dim
[1] 200

$costp_l1
[1] 0

$costp_l2
[1] 0.1

$costq_l1
[1] 0

$costq_l2
[1] 1

$lrate
[1] 0.1

$loss_fun
[1] 3.907436

Now let’s learn the model on full train data using the best parameters computed above

r2$train(data_memory(train$user_id,train$product_id,train$count), opts = c(opts$min, nthread = 4, niter = 100))

Now let’s pad the test data with 100 random samples for each user and evaluate the model and predict the ratings. Since we downsampled the data, padding with 100 random samples instead of 1000 should be sufficient.

test_padded = padTestData(test,train,1000)
reco2 = r2$predict(data_memory(test_padded$user_id, test_padded$product_id), out_memory())
ranks_MF = evaluateModel2(test_padded,reco2)
cat("Mean MPR across all users (MF) = ", mean(ranks_MF),"\n")
Mean MPR across all users (MF) =  27.80224 
cat("Median MPR across all users (MF) = ", median(ranks_MF),"\n")
Median MPR across all users (MF) =  24.70588 

Looks like the mean and median are not too far apart. Let’s look at the histogram of all users’ PR

ggplot(as.data.frame(ranks_MF), aes(ranks_MF, fill= ranks_MF > 50)) + geom_histogram(binwidth = 1) + xlab("Individual Percentile Ranking") + ylab("Number of users")

cat("Number of users with better than random predictions =", length(subset(ranks_MF,ranks_MF<50)), "\n")
Number of users with better than random predictions = 9834 
cat("Number of users with worse than random predictions =", length(subset(ranks_MF,ranks_MF>=50)), "\n")
Number of users with worse than random predictions = 1429 

Let’s also look at the scatter plot between individual user PRs and their frequency of purchase

pred_MF = data.frame(test_padded[,1],test_padded[,2],reco2)
users_MF = data.frame(unique(pred_MF$user_id),ranks_MF)
user_counts_test$ranks_MF = users_MF$ranks_MF
ggplot(user_counts_test) + geom_point(aes(x=total, y=ranks_MF, color = ranks_MF > 50)) + xlab("Total purchases") + ylab("Individual Percentile Ranking")

Observation: It is very evident that the performance of MF is poor mainly for users who are infrequent buyers, which was not the case for ALS. This is expected because ALS is tuned for implicit feedback data where an infrequent user doesn’t correspond to a user who “dislikes” those product. While the MF would think that infrequent users “dislike” that product and would therefore make wrong predictions about what they might buy in the future. In particular, this would mean that the MF algorithm would NOT recommend products which are similar to the ones the infrequent users already bought – which is not correct

Almost all of the cases where the performance is worse than 50% are for users who have bought less than 25 products (upper left corner). Like discussed earlier, a better down-sampling scheme which results in a more dense matrix might yield better performances where we can leverage more from frequent (power) users

cat('% of users who bought < 25 products (overall) =', nrow(subset(user_counts_test,total<25))/nrow(user_counts_test)*100, "%\n")
% of users who bought < 25 products (overall) = 92.06251 %
cat('% of users who bought < 25 products (with PR > 50%) =', nrow(subset(user_counts_test,total<25 & ranks_MF > 50))/nrow(subset(user_counts_test, ranks_MF > 50))*100, "%\n")
% of users who bought < 25 products (with PR > 50%) = 97.4212 %

Summary

Data:

  • Number of users: 11,263
  • Number of products: 3,138
  • Train data points: 79,307 (68%)
  • Test data points: 37,328 (32%)

Output:

  • Mean Percentile Rank using Weighted Alternating Least Squares method (Hu et. al.): 13.49% (Median: 7.41%)
  • Mean Percentile Rank using Matrix Factorization: 27.80% (Median: 24.71%)

Discussion

The experiments performed here are exploratory in nature and do not reflect the true potential of these algorithms. There is a lot of room for improvement:

  1. Data set: We used only a small subset of the data (about 5.5% of users and 6.5% of products). The goal of my experiments were to make them runnable on a single computer. By deploying these algorithms on a spark cluster over the entire data can give much better results.
  2. Sampling: Like described above, we can use a hybrid sampling approach so that even with the small subset of data, we can get a better representation of different types of users and products based on their frequency. This might help improve the results further.
  3. Parameter tuning: Better/more parameter tuning could have resulted in better MPRs:
    • The number of combinations grow exponentially with each new value of a hyperparameter which affects the computation time for cross-validation. We could do more optimized parameter tuning using coarse to fine methods.
    • For Alternating Least Squares method, we didn’t do traditional cross-validation, but a variant of it using smaller data sets. Traditional cross-validation could have yieled better hyperparameter estimates because it works on larger slices of train/validation data.
    • For Matrix Factorization method, we used the built-in parameter tuning which doesn’t optimize the MPR. Writing our own methods for doing that might have given better hyperparameter estimates. We could also try minimizing the L1 loss instead of L2 for both the lambda values
  4. Algorithms: We could also look at the different formulations of the algorithms. An important variation involves including the bias terms for every user and product. This would encode certain users’ preference to buy more/diverse set of products vs others and certain products being generally more popular/important than others. The ALS algorithm can be easily modified to introduce these biases (like suggested here). Likewise, Johnson’s approach using Logistic Matrix Factorization can also be used. That said, I believe the biggest gains will likely come from using more data.
LS0tCnRpdGxlOiAiSW5zdGFjYXJ0IHJlY29tbWVuZGF0aW9uIHByb2JsZW0iCm91dHB1dDoKICBodG1sX25vdGVib29rOiBkZWZhdWx0CiAgaHRtbF9kb2N1bWVudDogZGVmYXVsdAogIHBkZl9kb2N1bWVudDogZGVmYXVsdAotLS0KCiMjIyBQcm9ibGVtIGRlZmluaXRpb24KClRoZSBbZGF0YSBzZXRdKGh0dHBzOi8vd3d3Lmluc3RhY2FydC5jb20vZGF0YXNldHMvZ3JvY2VyeS1zaG9wcGluZy0yMDE3KSBkZXNjcmliZXMgY3VzdG9tZXIgb3JkZXJzIG92ZXIgdGltZS4gVGhlIGdvYWwgb2YgdGhlIGV4cGVyaW1lbnQgaXMgdG8gcHJlZGljdCB3aGljaCBwcm9kdWN0cyB1c2VycyB3aWxsIGJ1eSBuZXh0LCBiYXNlZCBvbiB0aGVpciBwdXJjaGFzZSBoaXN0b3J5LiBGb3IgY29udmVuaWVuY2UsIHRoZSBvcmlnaW5hbCBrYWdnbGUgcHJvYmxlbSBoYXMgYmVlbiBzbGlnaHRseSByZWZvcm11bGF0ZWQ6IHdlIHNob3VsZCBvbmx5IGNvbnNpZGVyIFw8dXNlcmlkLCBwcm9kdWN0aWRcPiBwYWlycyBhbmQgZGlzY2FyZCBhbnkgb3RoZXIgYXNzb2NpYXRlZCBmZWF0dXJlcyBsaWtlIGRheSBvZiB3ZWVrIGFuZCBob3VyIG9mIGRheSBvZiB0aGUgcHVyY2hhc2UuIExldCdzIGdldCBzdGFydGVkIQoKVGhlIHR3byBtYWluIHBhY2thZ2VzIHRoYXQgd2Ugd2lsbCBiZSB1c2luZyBhcmUgInJlY29tbWVuZGVybGFiIiBhbmQgInJlY29zeXN0ZW0iLiBUaGVzZSBwYWNrYWdlcyBwcm92aWRlIGRpZmZlcmVudCBhbGdvcml0aG1zIGZvciBjb2xsYWJvcmF0aW9uIGZpbHRlcmluZyAobWF0cml4IGNvbXBsZXRpb24pLiBNb3JlIGluZm8gY2FuIGJlIGZvdW5kIGhlcmU6CgoxLiBbcmVjb21tZW5kZXJsYWIgcGFja2FnZV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3JlY29tbWVuZGVybGFiL2luZGV4Lmh0bWwpCjIuIFtyZWNvc3lzdGVtIHBhY2thZ2VdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9yZWNvc3lzdGVtL2luZGV4Lmh0bWwpCgo8YnI+CgojIyMjIExvYWQvaW5zdGFsbCBsaWJyYXJpZXMKTGV0J3MgbG9hZCB0aGUgcmVxdWlyZWQgbGlicmFyaWVzIGFuZCBzb3VyY2UgbWV0aG9kcyBmaXJzdC4gCgoqKk5vdGU6KioKV2Ugd2lsbCB1c2UgdGhlIGxhdGVzdCAoYmV0YSkgdmVyc2lvbiBvZiB0aGUgInJlY29tbWVuZGVybGFiIiBwYWNrYWdlLiBZb3Ugc2hvdWxkIGRpcmVjdGx5IGluc3RhbGwgaXQgZnJvbSBnaXRodWIgYW5kIHNob3VsZCBOT1QgdXNlICJpbnN0YWxsLnBhY2thZ2UiIChzZWUgY29tbWVudGVkIGNvZGUgYmVsb3cpCmBgYHtyfQpyZXF1aXJlKGRwbHlyKQpyZXF1aXJlKGRhdGEudGFibGUpCgojIGxpYnJhcnkoZGV2dG9vbHMpCiMgCiMgaWYoInJlY29tbWVuZGVybGFiIiAlaW4lIHJvd25hbWVzKGluc3RhbGxlZC5wYWNrYWdlcygpKSA9PSBGQUxTRSl7CiMgICBpbnN0YWxsX2dpdGh1YigibWhhaHNsZXIvcmVjb21tZW5kZXJsYWIiKQojIH0KCnJlcXVpcmUocmVjb21tZW5kZXJsYWIpCnJlcXVpcmUocmVjb3N5c3RlbSkKcmVxdWlyZShnZ3Bsb3QyKQpyZXF1aXJlKGdyaWQpCnJlcXVpcmUoZ3JpZEV4dHJhKQoKc291cmNlKCJjcm9zc3ZhbGlkYXRlQUxTLlIiKQpzb3VyY2UoInBhcnRpdGlvbkRhdGEuUiIpCnNvdXJjZSgiY3Jvc3N2YWxpZGF0ZVVCQ0YuUiIpCnNvdXJjZSgiZXZhbHVhdGVNb2RlbDIuUiIpCnNvdXJjZSgicHJlcGFyZVRyYWluVGVzdERhdGEuUiIpCnNvdXJjZSgicGFkVGVzdERhdGEuUiIpCmBgYAo8YnI+CgojIyMjIFJlYWQgZGF0YQoKTmV4dCwgbGV0J3MgcmVhZCB0aGUgZGF0YSBmaWxlcyAoc3RvcmVkIGxvY2FsbHkgaW4gImluc3RhY2FydF8yMDE3XzA1XzAxIiBkaXJlY3RvcnkpCmBgYHtyfQphaXNsZXMgPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvYWlzbGVzLmNzdicpICAgICAgICAgICAgICMgMTM0IGFpc2xlcwpkZXBhcnRtZW50cyA9IGZyZWFkKCdpbnN0YWNhcnRfMjAxN18wNV8wMS9kZXBhcnRtZW50cy5jc3YnKSAgICMgMjEgZGVwYXJ0bWVudHMKcHJvZHVjdHMgPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvcHJvZHVjdHMuY3N2JykgICAgICAgICAjIH41MEsgcHJvZHVjdHMKb3JkZXJfcHJvZHVjdHNfcHJpb3IgPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvb3JkZXJfcHJvZHVjdHNfX3ByaW9yLmNzdicsa2V5PWMoIm9yZGVyX2lkIikpICAgICMgfjMyTSBpbmRpdmlkdWFsIHByb2R1Y3RzIG9yZGVyZWQKb3JkZXJfcHJvZHVjdHNfdHJhaW4gPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvb3JkZXJfcHJvZHVjdHNfX3RyYWluLmNzdicsa2V5PWMoIm9yZGVyX2lkIikpICAgICMgfjEuM00gaW5kaXZpZHVhbCBwcm9kdWN0cyBvcmRlcmVkIChpZ25vcmUpCm9yZGVycyA9IGZyZWFkKCdpbnN0YWNhcnRfMjAxN18wNV8wMS9vcmRlcnMuY3N2JyxrZXk9Yygib3JkZXJfaWQiKSkKYGBgCjxicj4KCiMjIyMgRGF0YSBwcmVwcm9jZXNzaW5nCiogV2Ugd2lsbCBjb25zaWRlciBvbmx5IHRoZSBvcmRlcnMgZGF0YSBtYXJrZWQgInByaW9yIi4gCiogVGhlIGRhdGEgc2V0IGlzIG5vcm1hbGl6ZWQsIHNvIHRvIG9idGFpbiBcPHVzZXJpZCwgcHJvZHVjdGlkPiBwYWlycywgd2UgbmVlZCB0byBqb2luICJvcmRlcnNfcHJpb3IiIGFuZCAib3JkZXJfcHJvZHVjdHNfcHJpb3IiIHRhYmxlcyBvbiB0aGUga2V5ICJvcmRlcl9pZCIuIAoqIFdlIHdpbGwgdGhlbiBzb3J0IHRoZSByZXN1bHRpbmcgdGFibGUgKCJvcmRlcl9hbGwiKSBvbiAidXNlcl9pZCIgYW5kICJwcm9kdWN0X2lkIiBrZXlzIAoqIEZyb20gdGhpcywgd2UnbGwgb2J0YWluICJkYXRhX2luZCIgd2hpY2ggaXMgYSBsaXN0IG9mIGluZGl2aWR1YWwgXDx1c2VyX2lkLCBwcm9kdWN0X2lkXD4gcGFpcnMKKiBGcm9tICJkYXRhX2luZCIsIHdlIGNhbiBhY2N1bXVsYXRlIHNpbWlsYXIgXDx1c2VyX2lkLCBwcm9kdWN0X2lkXD4gcGFpcnMgYW5kIGZvcm0gImRhdGEiIHRhYmxlIHdoaWNoIGNvbnRhaW5zIFw8dXNlcl9pZCwgcHJvZHVjdF9pZCwgY291bnRcPiB0cmlwbGVzCmBgYHtyfQpvcmRlcnNfcHJpb3IgPSBzdWJzZXQob3JkZXJzLGV2YWxfc2V0PT0icHJpb3IiKSAgICAgICAgICMgfjMuMk0gInByaW9yIiBvcmRlcnMKb3JkZXJzX2FsbCA9IG1lcmdlKG9yZGVyc19wcmlvcixvcmRlcl9wcm9kdWN0c19wcmlvciwgYnk9Im9yZGVyX2lkIikKCm9yZGVyc19hbGwgPSBvcmRlcnNfYWxsW29yZGVyKG9yZGVyc19hbGwkdXNlcl9pZCxvcmRlcnNfYWxsJHByb2R1Y3RfaWQpLF0KCmRhdGFfaW5kID0gb3JkZXJzX2FsbFssYygidXNlcl9pZCIsICJwcm9kdWN0X2lkIildCgpzZXRrZXkoZGF0YV9pbmQsdXNlcl9pZCxwcm9kdWN0X2lkKQoKZGF0YSA9IGFnZ3JlZ2F0ZShjYmluZChjb3VudCA9IHByb2R1Y3RfaWQpIH4gcHJvZHVjdF9pZCt1c2VyX2lkLCBkYXRhPWRhdGFfaW5kLCBGVU4gPSBsZW5ndGgpCmRhdGEgPSBzZXREVChkYXRhWyxjKDIsMSwzKV0pCmBgYAo8YnI+CgojIyMjIEdlbmVyYXRpbmcgdHJhaW4vdGVzdCBkYXRhCgpUaGUgZGF0YSBzZXQgaXMgdG9vIGJpZyB0byB3b3JrIHdpdGggb24gYSBzdGFuZGFsb25lIGNvbXB1dGVyLCBzbyB3ZSBhcmUgZ29pbmcgdG8gZG93bnNhbXBsZSBpdDsgYWx0aG91Z2ggaWRlYWxseSwgd2UgY2FuIGhvcGUgdG8gZ2V0IGJlc3QgcGVyZm9ybWFuY2UgdXNpbmcgZnVsbCBkYXRhLiBMZXQncyBzZWUgaG93IHRoZSBkaXN0cmlidXRpb24gb2YgdG90YWwgcHJvZHVjdCBwdXJjaGFzZXMgYWNyb3NzIGFsbCB1c2VycyBsb29rcyBsaWtlCgpgYGB7cn0KcHJvZHVjdF9jb3VudHMgPSBhZ2dyZWdhdGUoY2JpbmQodG90YWwgPSBjb3VudCkgfiBwcm9kdWN0X2lkLCBkYXRhPWRhdGFbLDI6M10sIEZVTiA9IHN1bSkKZ2dwbG90KHByb2R1Y3RfY291bnRzLCBhZXModG90YWwpKSArIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMTAwMCkgKyB4bGFiKCJudW1iZXIgb2YgdGltZXMgYm91Z2h0IikKYGBgCgpBcyBleHBlY3RlZCwgdGhpcyBsb29rcyBoaWdobHkgbm9uLXVuaWZvcm0uIExldCdzIGxvb2sgYXQgdHdvIGRpZmZlcmVudCByYW5nZXMgZm9yIGJldHRlciB1bmRlcnN0YW5kaW5nOiBwcm9kdWN0cyB0aGF0IGFyZSBib3VnaHQgbGVzcyB0aGFuIDIwMDAgdGltZXMgYW5kIG9uZXMgYm91Z2h0IGJldHdlZW4gMjAwMCB0byAyMDAwMCB0aW1lcwoKYGBge3J9CnAxID0gZ2dwbG90KHN1YnNldChwcm9kdWN0X2NvdW50cyx0b3RhbDwyMDAwKSwgYWVzKHRvdGFsKSkgKyBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDEwKSArIHhsYWIoIm51bWJlciBvZiB0aW1lcyBib3VnaHQiKQpwMiA9IGdncGxvdChzdWJzZXQocHJvZHVjdF9jb3VudHMsdG90YWw+MjAwMCAmIHRvdGFsIDwgMjAwMDApLCBhZXModG90YWwpKSArIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMTAwKSArIHhsYWIoIm51bWJlciBvZiB0aW1lcyBib3VnaHQiKSArIHhsaW0oMTAwMCwyMDAwMCkKZ3JpZC5hcnJhbmdlKHAxLCBwMiwgbmNvbCA9IDIpCmBgYAoKU2ltaWxhcmx5LCBsZXQncyBwbG90IHRoZSBudW1iZXIgb2YgZnJlcXVlbmN5IG9mIGRpZmZlcmVudCB1c2VycyBpbiB0aGUgZGF0YToKCmBgYHtyfQp1c2VyX2NvdW50cyA9IGFnZ3JlZ2F0ZShjYmluZCh0b3RhbCA9IGNvdW50KSB+IHVzZXJfaWQsIGRhdGE9ZGF0YVssYygxOjMpXSwgRlVOID0gc3VtKQpnZ3Bsb3QodXNlcl9jb3VudHMsIGFlcyh0b3RhbCkpICsgZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSAxMCkgKyB4bGFiKCJUb3RhbCBudW1iZXIgb2YgcHJvZHVjdHMgYm91Z2h0IGJ5IGEgdXNlciIpCmBgYApBcyB3ZSBjYW4gc2VlIHRoYXQgdGhlIG51bWJlciBvZiBwcm9kdWN0cyBhbmQgdXNlcnMgZGVjcmVhc2Ugc2hhcnBseSB3aXRoIHRoZWlyIGZyZXF1ZW5jeS4gSW4gb3RoZXIgd29yZHM6CgoqIE1vc3QgcHJvZHVjdHMgd2VyZSBib3VnaHQgcmFyZWx5LCB3aGlsZSBvbmx5IGEgdmVyeSBmZXcgcHJvZHVjdHMgd2VyZSBmcmVxdWVudGx5IGJvdWdodAoqIE1vc3QgdXNlcnMgYm91Z2h0IHZlcnkgZmV3IHByb2R1Y3RzLCB3aGlsZSB2ZXJ5IGZldyB1c2VycyBib3VnaHQgYSBsb3Qgb2YgcHJvZHVjdHMKCkdpdmVuIHRoaXMsIHRoZXJlIGFyZSBkaWZmZXJlbnQgd2F5cyBpbiB3aGljaCB3ZSBjYW4gZG93bi1zYW1wbGUgdGhlIGRhdGE6CgoqICoqU2NoZW1lIDEgKFVuaWZvcm0gc2FtcGxpbmcpOioqIFJhbmRvbWx5IHBpY2sgKlAqIHByb2R1Y3RzIGFuZCAqVSogdXNlcnMKICAgICsgKlByb3M6KiBEYXRhIGRpc3RyaWJ1dGlvbnMgb2YgcG9wdWxhciB2cy4gcmFyZWx5LWJvdWdodCBwcm9kdWN0cyBhbmQgZnJlcXVlbnQgdnMuIGluZnJlcXVlbnQgdXNlcnMgYXJlIHByZXNlcnZlZAogICAgKyAqQ29uczoqIFdpdGggc21hbGwgKlAqIGFuZCAqVSosIHdlIG1pZ2h0IGVuZCB1cCB3aXRoIG1vc3RseSByYXJlbHkgYm91Z2h0IHByb2R1Y3RzIGFuZCBpbmZyZXF1ZW50IHVzZXJzLCBiZWNhdXNlIHRob3NlIGFyZSB0aGUgb25lcyB3aXRoIHZhc3QgbWFqb3JpdHkuCiAgICAKKiAqKlNjaGVtZSAyIChXZWlnaHRlZCBzYW1wbGluZyk6KiogUGljayAqUCogcHJvZHVjdHMgYW5kICpVKiB1c2VycyBiYXNlZCBvbiB0aGVpciBmcmVxdWVuY3kgb2YgcHVyY2hhc2UKICAgICsgKlByb3M6KiBXZSB3aWxsIGdldCBtb3JlIGZyZXF1ZW50IHVzZXJzIGFuZCBwb3B1bGFyIHByb2R1Y3RzLCBzbyB0aGUgZGF0YSBtYXRyaXggd2lsbCBiZSBtb3JlIGRlbnNlLCB0aHVzIGluY3JlYXNpbmcgdGhlIHByZWRpY3RpdmUgcG93ZXIgb2YgdGhlIGFsZ29yaXRobQogICAgKyAqQ29uczoqIE9yaWdpbmFsIGRhdGEgZGlzdHJpYnV0aW9ucyBhcmUgbm90IHByZXNlcnZlZC4gU28gcGVyZm9ybWFuY2UgbWlnaHQgYmUgcG9vciByYXJlbHktYm91Z2h0IHByb2R1Y3RzIGFuZCBpbmZyZXF1ZW50IHVzZXJzCiAgICAKKiAqKlNjaGVtZSAzIChIeWJyaWQgc2FtcGxpbmcpOioqIFBpY2sgKlAqIHByb2R1Y3RzIGFuZCAqVSogdXNlcnMgYmFzZWQgb24gc29tZSBjb21iaW5hdGlvbiBvZiBTY2hlbWUgMSBhbmQgMiBhYm92ZS4gRm9yIGV4YW1wbGUsIHdlIGNhbiBkaXZpZGUgdGhlIHByb2R1Y3RzIGFuZCB1c2VycyBpbnRvICpOKiBwZXJjZW50aWxlcyBiYXNlZCBvbiB0aGVpciBmcmVxdWVuY3kgYW5kIHRoZW4gdW5pZm9ybWx5IHNhbXBsZSBmcm9tIGVhY2ggYmluCiAgICArICpQcm9zOiogV2Ugd2lsbCBnZXQgYSBnb29kIG1peCBvZiBhbGwgdHlwZXMgb2YgdXNlcnMgYW5kIHByb2R1Y3RzLCBzbyBvdmVyYWxsIHByZWRpY3RpdmUgcG93ZXIgc2hvdWxkIGJlIGJldHRlcgogICAgKyAqQ29uczoqIE9yaWdpbmFsIGRhdGEgZGlzdHJpYnV0aW9ucyBhcmUgbm90IHByZXNlcnZlZAogIAoqKkZvciBub3csIHdlJ2xsIGdvIHdpdGggU2NoZW1lIDEsIGluIGZhdm9yIG9mIHByZXNlcnZpbmcgdGhlIG9yaWdpbmFsIGRhdGEgZGlzdHJpYnV0aW9uIHdpdGggKlAqPTUwMDAgYW5kICpVKj0xNTAwMCoqIAoKYGBge3J9CmRhdGFfb3JpZyA9IGRhdGEgIyBzYXZlIHRoZSBvcmlnaW5hbCBkYXRhCgp1c2VycyA9IHVuaXF1ZShkYXRhJHVzZXJfaWQpCnVzZXJfc2FtcGxlcyA9IHVzZXJzW3NhbXBsZShsZW5ndGgodW5pcXVlKGRhdGEkdXNlcl9pZCkpLCAxNTAwMCwgcmVwbGFjZSA9IEZBTFNFKV0KZGF0YSA9IHN1YnNldChkYXRhLHVzZXJfaWQgJWluJSB1c2VyX3NhbXBsZXMpCgpwcm9kdWN0cyA9IHVuaXF1ZShkYXRhJHByb2R1Y3RfaWQpCnByb2R1Y3Rfc2FtcGxlcyA9IHByb2R1Y3RzW3NhbXBsZShsZW5ndGgodW5pcXVlKGRhdGEkcHJvZHVjdF9pZCkpLCA1MDAwLCByZXBsYWNlID0gRkFMU0UpXQpkYXRhID0gc3Vic2V0KGRhdGEscHJvZHVjdF9pZCAlaW4lIHByb2R1Y3Rfc2FtcGxlcykKCmBgYAoKKiBOb3csIHBhcnRpdGlvbiB0aGUgZGF0YSBpbnRvIHRyYWluL3Rlc3QgKDcwLzMwKSBtYWtpbmcgc3VyZSB0aGF0IHRoZSBmb2xsb3dpbmcgY29uZGl0aW9ucyBhcmUgbWV0OgogICAgMS4gQWxsIHByb2R1Y3QgaWQncyBhbmQgdXNlciBpZCdzIGFyZSB0aGVyZSBpbiBib3RoIHRyYWluIGFuZCB0ZXN0IHNldHMKICAgIDIuIEEgcGFpciBcPHVzZXJpZCwgcHJvZHVjdGlkXD4gY2FuIGVpdGhlciBhcHBlYXIgaW4gdHJhaW4gb3IgdGVzdCBzZXQsIGJ1dCBub3QgYm90aAoqIEluICJwYXJ0aXRpb25EYXRhIiBtZXRob2QsIHdlIHBhcnRpdGlvbiB0aGUgZGF0YSBpbiB0d28gc3RlcHM6CiAgICAxLiBSYW5kb21seSBwYXJ0aXRpb24gImRhdGEiIHRhYmxlIGludG8gInRyYWluIiBhbmQgInRlc3QiIHRhYmxlcyBhcyBwZXIgdGhlIHJlcXVpcmVkIHNwbGl0IHJhdGlvCiAgICAyLiBSZW1vdmUgdXNlcnMvcHJvZHVjdHMgdGhhdCBhcmUgdGhlcmUgaW4gb25lIHNldCBhbmQgbm90IGluIGFub3RoZXIKICAgIAogICAgKipOb3RlOioqIFRoaXMgaXMgbm90IHRoZSBiZXN0IHdheSB0byBzcGxpdCB0aGUgZGF0YSwgYW5kIGluY3VycyBzb21lIGxvc3MuIEEgYmV0dGVyIGFwcHJvYWNoIHdvdWxkIGJlIHRvIHBhcnRpdGlvbiBpdCBiYXNlZCBvbiB0aGUgam9pbnQgZGlzdHJpYnV0aW9uIG9mIHVzZXJzIGFuZCBwcm9kdWN0cyBpbiAiZGF0YSIKCmBgYHtyfQpkYXRhX3BhcnRzID0gcGFydGl0aW9uRGF0YShkYXRhID0gZGF0YSwgZnJhY0RhdGEgPSAxLCBmcmFjVHJhaW4gPSAwLjcpCnRyYWluID0gZGF0YV9wYXJ0cyR0cmFpbgp0ZXN0ID0gZGF0YV9wYXJ0cyR0ZXN0CmBgYAoKT2ssIG5vdyB3ZSBzaG91bGQgaGF2ZSBhIHRyYWluL3Rlc3Qgc3BsaXQgd2hlcmUgYm90aCBjb25kaXRpb25zIDEgJiAyIGFyZSBzYXRpc2ZpZWQuIExldCdzIGNoZWNrIHNwbGl0IHJhdGlvIGFnYWluCmBgYHtyfQpjYXQoIlRyYWluICUgPSAiLCBucm93KHRyYWluKS8obnJvdyh0cmFpbikgKyBucm93KHRlc3QpKSoxMDAsICJhbmQgdGVzdCAlID0gIiwgbnJvdyh0ZXN0KS8obnJvdyh0cmFpbikgKyBucm93KHRlc3QpKSoxMDAsICJcbiIpCmNhdCgibnJvd3MgdHJhaW4gPSAiLCBucm93KHRyYWluKSwgIlxuIikKY2F0KCJucm93cyB0ZXN0ID0gIiwgbnJvdyh0ZXN0KSwgIlxuIikKY2F0KCJOdW1iZXIgb2YgdW5pcXVlIHVzZXJfaWQncyA9ICIsIGxlbmd0aCh1bmlxdWUodHJhaW4kdXNlcl9pZCkpLCAiXG4iKQpjYXQoIk51bWJlciBvZiB1bmlxdWUgcHJvZHVjdF9pZCdzID0gIiwgbGVuZ3RoKHVuaXF1ZSh0cmFpbiRwcm9kdWN0X2lkKSksICJcbiIpCmBgYAoKQWxzbywgbGV0J3MgcXVpY2tseSBjb21wYXJlIHRoZSBzcGFyc2l0eSBvZiB0aGUgdHJhaW4vdGVzdCBkYXRhIHcuci50LiBvcmlnaW5hbCBkYXRhCmBgYHtyfQpjYXQoIlNwYXJzaXR5IGluIG9yaWdpbmFsIGRhdGEgPSAiLCAxMDAgLSBucm93KGRhdGFfb3JpZykvKGFzLmRvdWJsZShsZW5ndGgodW5pcXVlKGRhdGFfb3JpZyRwcm9kdWN0X2lkKSkpKmFzLmRvdWJsZShsZW5ndGgodW5pcXVlKGRhdGFfb3JpZyR1c2VyX2lkKSkpKSoxMDAsICIlXG4iKQpjYXQoIlNwYXJzaXR5IGluIHRyYWluIGRhdGEgPSAiLCAxMDAgLSBucm93KHRyYWluKS8oYXMuZG91YmxlKGxlbmd0aCh1bmlxdWUodHJhaW4kcHJvZHVjdF9pZCkpKSphcy5kb3VibGUobGVuZ3RoKHVuaXF1ZSh0cmFpbiR1c2VyX2lkKSkpKSoxMDAsICIlXG4iKQpjYXQoIlNwYXJzaXR5IGluIHRlc3QgZGF0YSA9ICIsIDEwMCAtIG5yb3codGVzdCkvKGFzLmRvdWJsZShsZW5ndGgodW5pcXVlKHRlc3QkcHJvZHVjdF9pZCkpKSphcy5kb3VibGUobGVuZ3RoKHVuaXF1ZSh0ZXN0JHVzZXJfaWQpKSkpKjEwMCwgIiVcbiIpCmBgYApTbyBpdCBpcyByb3VnaGx5IHRoZSBzYW1lLCB3aGljaCBpcyB3aGF0IHdlIGV4cGVjdCB3aXRoIHVuaWZvcm0gc2FtcGxpbmcgYXBwcm9hY2gKCjxicj4KCiMjIyMgRXhwbG9yaW5nIFdlaWdodGVkIEFsdGVybmF0aW5nIExlYXN0IFNxdWFyZSBhcHByb2FjaCAodmlhICJyZWNvbW1lbmRlcmxhYiIgcGFja2FnZSkKVGhlICJyZWNvbW1lbmRlcmxhYiIgcGFja2FnZSBpbXBsZW1lbnRzIHRoZSBBTFMgKGFsdGVybmF0aW5nIGxlYXN0IHNxdWFyZXMpIG9wdGltaXphdGlvbiBhcHByb2FjaCBkZXNjcmliZWQgaW4gW0h1IGV0LiBhbC4ncyBwYXBlcl0oaHR0cDovL3lpZmFuaHUubmV0L1BVQi9jZi5wZGYpCgoqKk5vdGUgMToqKiBUaGUgcGFja2FnZSAqaGFzKiBhIHJvdXRpbmUgdG8gdHVuZSBkaWZmZXJlbnQgcGFyYW1ldGVyczogdGhlIHJlZ3VsYXJpemF0aW9uIHBhcmFtZXRlciwgbGFtYmRhIGFuZCBsZW5ndGggb2YgdGhlIGxhdGVudCBmYWN0b3JzLCAqZCouIEJ1dCB0aGUgdHVuaW5nIGlzIHBlcmZvcm1lZCB3aGlsZSBvcHRpbWl6aW5nIGZvciBNU0UgKG1lYW4gc3F1YXJlZCBlcnJvciksIE1BRSAobWVhbiBhYnNvbHV0ZSBlcnJvcikgb3IgUk1TRSAocm9vdCBtZWFuIHNxdWFyZWQgZXJyb3IpLiBIb3dldmVyLCBzaW5jZSB3ZSBhcmUgaW50ZXJlc3RlZCBpbiBtaW5pbWl6aW5nIHRoZSBNUFIgKG1lYW4gcGVyY2VudGlsZSByYW5raW5nKSwgd2UnbGwgdXNlIG91ciBvd24gbWV0aG9kIGZvciB0dW5pbmcgdGhlc2UgcGFyYW1ldGVycwoKKipOb3RlIDI6KiogSWRlYWxseSwgZm9yIGNyb3NzLXZhbGlkYXRpb24sIHdlIHNob3VsZCBwYXJ0aXRpb24gdGhlIHRyYWluaW5nIGRhdGEgaW50byAqZiogZm9sZHMgYW5kIHVzZSBsZWF2ZS1vbmUtZm9sZC1vdXQgdGVjaG5pcXVlIHRvIHR1bmUgdGhlIHBhcmFtZXRlcnMuIEhvd2V2ZXIsIGluIGZhdm9yIG9mIHR1bmluZyB0aGUgcGFyYW1ldGVycyBmYXN0ZXIsIHdlIHdpbGwgcmVzYW1wbGUgdGhlICJ0cmFpbiIgZGF0YSBtdWx0aXBsZSB0aW1lcyB0byBnZW5lcmF0ZSB0cmFpbmluZy92YWxpZGF0aW9uIHNldHMgYW5kIHNpbXBseSBhdmVyYWdlIHRoZSBNUFIgb3ZlciBkaWZmZXJlbnQgcnVucyAoZm9sZHMpIGZvciBlYWNoIHNldCBvZiBwYXJhbWV0ZXJzCgpgYGB7cn0Kc291cmNlKCJldmFsdWF0ZU1vZGVsLlIiKQpzb3VyY2UoImNyb3NzdmFsaWRhdGVBTFMuUiIpCmxhbWJkYSA9IGMoMC4wMSwxLDEwKQpuRmFjdG9ycyA9IGMoMTAsMTAwLDIwMCkKYWxwaGEgPSBjKDEsIDEwLCAxMDApCnJhbmtzX0FMUyA9IGNyb3NzdmFsaWRhdGVBTFMoZGF0YSA9IHRyYWluLCBsYW1iZGEgPSBsYW1iZGEsIG5GYWN0b3JzID0gbkZhY3RvcnMsIGFscGhhID0gYWxwaGEsIGZyYWNEYXRhID0gMC4yLCBjdkZvbGRzID0gNCwgbkl0ZXIgPSAxMCwgZnJhY1RyYWluID0gMC43KQpgYGAKCgpgYGB7cn0KcmFua3NfQUxTCmBgYAoKYGBge3J9CmxhbWJkYV9iZXN0ID0gbGFtYmRhW3doaWNoKHJhbmtzX0FMUyA9PSBtaW4ocmFua3NfQUxTKSwgYXJyLmluZCA9IFRSVUUpWzFdXQpuRmFjdG9yc19iZXN0ID0gbkZhY3RvcnNbd2hpY2gocmFua3NfQUxTID09IG1pbihyYW5rc19BTFMpLCBhcnIuaW5kID0gVFJVRSlbMl1dCmFscGhhX2Jlc3QgPSBuRmFjdG9yc1t3aGljaChyYW5rc19BTFMgPT0gbWluKHJhbmtzX0FMUyksIGFyci5pbmQgPSBUUlVFKVszXV0KCmNhdCgiYmVzdCBsYW1iZGEgdmFsdWUgPSIsIGxhbWJkYV9iZXN0LCAiYW5kIGJlc3QgZCB2YWx1ZSA9IiwgbkZhY3RvcnNfYmVzdCwgImFuZCBiZXN0IGFscGhhIHZhbHVlID0iLCBhbHBoYV9iZXN0LCAiXG4iKQpgYGAKCk5vdyB0aGF0IHdlIGhhdmUgdGhlIG9wdGltYWwgcGFyYW1ldGVycywgbGV0cyB0cmFpbiBvbiBmdWxsIHRyYWluaW5nIGRhdGEgdXNpbmcgdGhlIG9wdGltYWwgcGFyYW1ldGVyIHZhbHVlcyBjb21wdXRlZCBhYm92ZSBhbmQgY29tcHV0ZS9ldmFsdWF0ZSB0aGUgcHJlZGljdGlvbnMuIE5vdGUgdGhhdCwgc2luY2Ugd2UgZG93bnNhbXBsZWQgdGhlIGRhdGEsIHBhZGRpbmcgd2l0aCAxMDAgcmFuZG9tIHNhbXBsZXMgaW5zdGVhZCBvZiAxMDAwIHNob3VsZCBiZSBzdWZmaWNpZW50CgpgYGB7cn0KUiA9IGFzKHRyYWluLCAicmVhbFJhdGluZ01hdHJpeCIpCnIgPSBSZWNvbW1lbmRlcihSLCBtZXRob2QgPSAiQUxTX2ltcGxpY2l0IiwgcGFyYW1ldGVyID0gbGlzdChuX2ZhY3RvcnM9bkZhY3RvcnNfYmVzdCwgbGFtYmRhPWxhbWJkYV9iZXN0LCBhbHBoYT1hbHBoYV9iZXN0LCBuX2l0ZXJhdGlvbnM9MjAsIHNlZWQgPSBOVUxMLCB2ZXJib3NlID0gVFJVRSkpIApyZWNvID0gcHJlZGljdChyLCBSLCB0eXBlID0gInJhdGluZ3MiKQpyYW5rX2ZpbmFsX3JlY29sYWIgPSBldmFsdWF0ZU1vZGVsKHJlY28gPSByZWNvLCB0ZXN0ID0gdGVzdCwgcGFkID0gMTAwKQpgYGAKCmBgYHtyfQpjYXQoIk1lYW4gTVBSIGFjcm9zcyBhbGwgdXNlcnMgPSAiLCBtZWFuKHJhbmtfZmluYWxfcmVjb2xhYiksIlxuIikKY2F0KCJNZWRpYW4gTVBSIGFjcm9zcyBhbGwgdXNlcnMgPSAiLCBtZWRpYW4ocmFua19maW5hbF9yZWNvbGFiKSwiXG4iKQpgYGAKTGV0J3MgYWxzbyBsb29rIGF0IHRoZSBmdWxsIGhpc3RvZ3JhbSBvZiBpbmRpdmlkdWFsIFBSJ3MKYGBge3J9CmdncGxvdChhcy5kYXRhLmZyYW1lKHJhbmtfZmluYWxfcmVjb2xhYiksIGFlcyhyYW5rX2ZpbmFsX3JlY29sYWIsIGZpbGw9IHJhbmtfZmluYWxfcmVjb2xhYiA+IDUwKSkgKyBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDEpICsgeGxhYigiSW5kaXZpZHVhbCBQZXJjZW50aWxlIFJhbmtpbmciKSArIHlsYWIoIk51bWJlciBvZiB1c2VycyIpCmBgYApgYGB7cn0KY2F0KCJOdW1iZXIgb2YgdXNlcnMgd2l0aCBiZXR0ZXIgdGhhbiByYW5kb20gcHJlZGljdGlvbnMgPSIsIGxlbmd0aChzdWJzZXQocmFua19maW5hbF9yZWNvbGFiLHJhbmtfZmluYWxfcmVjb2xhYjw1MCkpLCAiXG4iKQpjYXQoIk51bWJlciBvZiB1c2VycyB3aXRoIHdvcnNlIHRoYW4gcmFuZG9tIHByZWRpY3Rpb25zID0iLCBsZW5ndGgoc3Vic2V0KHJhbmtfZmluYWxfcmVjb2xhYixyYW5rX2ZpbmFsX3JlY29sYWI+PTUwKSksICJcbiIpCmBgYApMZXQncyBhbHNvIGxvb2sgYXQgdGhlIHNjYXR0ZXIgcGxvdCBiZXR3ZWVuIGluZGl2aWR1YWwgdXNlciBQUnMgYW5kIHRoZWlyIGZyZXF1ZW5jeSBvZiBwdXJjaGFzZQpgYGB7cn0KdXNlcl9jb3VudHNfdGVzdCA9IGFnZ3JlZ2F0ZShjYmluZCh0b3RhbCA9IGNvdW50KSB+IHVzZXJfaWQsIGRhdGE9dGVzdFssYygxOjMpXSwgRlVOID0gc3VtKQp1c2VyX2NvdW50c190ZXN0ID0gdXNlcl9jb3VudHNfdGVzdFtvcmRlcih1c2VyX2NvdW50c190ZXN0JHVzZXJfaWQpLF0KcHJlZF9BTFMgPSBnZXREYXRhLmZyYW1lKHJlY28sIGRlY29kZSA9IFRSVUUsIHJhdGluZ3MgPSBUUlVFKQp1c2Vyc19BTFMgPSBkYXRhLmZyYW1lKHVuaXF1ZShwcmVkX0FMUyR1c2VyKSxyYW5rX2ZpbmFsX3JlY29sYWIpCnVzZXJfY291bnRzX3Rlc3QkcmFua3NfQUxTID0gdXNlcnNfQUxTJHJhbmtfZmluYWxfcmVjb2xhYgpnZ3Bsb3QodXNlcl9jb3VudHNfdGVzdCkgKyBnZW9tX3BvaW50KGFlcyh4PXRvdGFsLCB5PXJhbmtzX0FMUywgY29sb3IgPSByYW5rc19BTFMgPiA1MCkpICsgeGxhYigiVG90YWwgcHVyY2hhc2VzIikgKyB5bGFiKCJJbmRpdmlkdWFsIFBlcmNlbnRpbGUgUmFua2luZyIpCmBgYApMb29rcyBsaWtlIHRoZSBsZWFybmluZyBpcyBxdWl0ZSB1bmlmb3JtIGFjcm9zcyBhbGwgdHlwZXMgb2YgdXNlcnMgKGZyZXF1ZW50IHZzIGluZnJlcXVlbnQgYnV5ZXJzKS4gTGV0J3MgYWxzbyBsb29rIGF0IHRoZSBwZXJjZW50YWdlcyBvZiB1c2VycyB3aG8gYm91Z2h0IDwgMjUgcHJvZHVjdHMKYGBge3J9CmNhdCgnJSBvZiB1c2VycyB3aG8gYm91Z2h0IDwgMjUgcHJvZHVjdHMgKG92ZXJhbGwpID0nLCBucm93KHN1YnNldCh1c2VyX2NvdW50c190ZXN0LHRvdGFsPDI1KSkvbnJvdyh1c2VyX2NvdW50c190ZXN0KSoxMDAsICIlXG4iKQpjYXQoJyUgb2YgdXNlcnMgd2hvIGJvdWdodCA8IDI1IHByb2R1Y3RzICh3aXRoIFBSID4gNTAlKSA9JywgbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCx0b3RhbDwyNSAmIHJhbmtzX0FMUyA+IDUwKSkvbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCwgcmFua3NfQUxTID4gNTApKSoxMDAsICIlXG4iKQpgYGAKVGhpcyB2ZXJpZmllcyB0aGUgZmFjdCB0aGF0IHRoZSBsZWFybmluZyBpcyBxdWl0ZSB1bmlmb3JtIGFjcm9zcyBkaWZmZXJlbnQgdHlwZXMgb2YgdXNlcnMKCjxicj4KCiMjIyMgRXhwbG9yaW5nIG1hdHJpeCBmYWN0b3JpemF0aW9uICh2aWEgInJlY29zeXN0ZW0iIHBhY2thZ2UpCgpMZXQncyBleHBsb3JlIHRoZSAicmVjb3N5c3RlbSIgbGlicmFyeS4gVGhpcyBwYWNrYWdlIGhhcyBhIG11Y2ggbW9yZSBlZmZpY2llbnQgaW1wbGVtZW50YXRpb24gdXNpbmcgU0dEIGFuZCBhbHNvIHN1cHBvcnRzIHBhcmFsbGVsL2NsdXN0ZXIgY29tcHV0YXRpb247IGFsdGhvdWdoIGZvciB0aGUgbW9yZSB0cmFkaXRpb25hbCBtYXRyaXggZmFjdG9yaXphdGlvbiwgd2hpY2ggKipkb2VzIG5vdCoqIG1vZGlmeSB0aGUgY29zdCBmdW5jdGlvbiBmb3IgaW1wbGljaXQgZmVlZGJhY2sgZGF0YQoKTm9uZXRoZWxlc3MsIEkgdGhvdWdodCBpdCB3b3VsZCBiZSBpbnRlcmVzdGluZyB0byBjb21wYXJlIGl0IHdpdGggdGhlIEFMUyBtZXRob2QuIEluIGFkZGl0aW9uIHRvIHR1bmluZyB0aGUgbnVtYmVyIG9mIGZhY3RvcnMsIGl0IHByb3ZpZGVzIGhvb2tzIHRvIHR1bmUgdGhlIGxlYXJuaW5nIHJhdGUgKHN0ZXAgc2l6ZSBpbiBTR0QpIGFuZCB0d28gc2VwYXJhdGUgcmVndWxhcml6YXRpb24gY29lZmZpY2llbnRzIGZvciB1c2VyIGFuZCBwcm9kdWN0IGZhY3RvcnMgZm9yIGJvdGggbDEgYW5kIGwyIG9wdGltaXplcnMuIAoKKipOb3RlOioqIExpa2UgZWFybGllciwgaWRlYWxseSwgd2Ugc2hvdWxkIG9wdGltaXplIGZvciBNUFIgd2hpbGUgZG9pbmcgcGFyYW1ldGVyIHR1bmluZy4gQnV0LCB3ZSdsbCBpZ25vcmUgdGhhdCBmb3Igbm93IGFuZCBvcHRpbWl6ZSB0aGUgZGVmYXVsdCBMMi1ub3JtCgpgYGB7cn0KcjIgPSBSZWNvKCkKCm9wdHMgPSByMiR0dW5lKGRhdGFfbWVtb3J5KHRyYWluJHVzZXJfaWQsdHJhaW4kcHJvZHVjdF9pZCx0cmFpbiRjb3VudCksIG9wdHMgPSBsaXN0KGRpbSA9IGMoMTAsMTAwLDIwMCksIGxyYXRlID0gYygwLjEsIDEsIDEwKSwgY29zdHBfbDIgPSBjKDAuMSwgMSwgMTApLCBjb3N0cV9sMiA9IGMoMC4xLCAxLCAxMCksIG50aHJlYWQgPSA0LCBuaXRlciA9IDIwKSkKYGBgCgpgYGB7cn0Kb3B0cyRtaW4KYGBgCgpOb3cgbGV0J3MgbGVhcm4gdGhlIG1vZGVsIG9uIGZ1bGwgdHJhaW4gZGF0YSB1c2luZyB0aGUgYmVzdCBwYXJhbWV0ZXJzIGNvbXB1dGVkIGFib3ZlCgpgYGB7cn0KcjIkdHJhaW4oZGF0YV9tZW1vcnkodHJhaW4kdXNlcl9pZCx0cmFpbiRwcm9kdWN0X2lkLHRyYWluJGNvdW50KSwgb3B0cyA9IGMob3B0cyRtaW4sIG50aHJlYWQgPSA0LCBuaXRlciA9IDEwMCkpCmBgYApOb3cgbGV0J3MgcGFkIHRoZSB0ZXN0IGRhdGEgd2l0aCAxMDAgcmFuZG9tIHNhbXBsZXMgZm9yIGVhY2ggdXNlciBhbmQgZXZhbHVhdGUgdGhlIG1vZGVsIGFuZCBwcmVkaWN0IHRoZSByYXRpbmdzLiBTaW5jZSB3ZSBkb3duc2FtcGxlZCB0aGUgZGF0YSwgcGFkZGluZyB3aXRoIDEwMCByYW5kb20gc2FtcGxlcyBpbnN0ZWFkIG9mIDEwMDAgc2hvdWxkIGJlIHN1ZmZpY2llbnQuCmBgYHtyfQp0ZXN0X3BhZGRlZCA9IHBhZFRlc3REYXRhKHRlc3QsdHJhaW4sMTAwMCkKcmVjbzIgPSByMiRwcmVkaWN0KGRhdGFfbWVtb3J5KHRlc3RfcGFkZGVkJHVzZXJfaWQsIHRlc3RfcGFkZGVkJHByb2R1Y3RfaWQpLCBvdXRfbWVtb3J5KCkpCnJhbmtzX01GID0gZXZhbHVhdGVNb2RlbDIodGVzdF9wYWRkZWQscmVjbzIpCmBgYAoKYGBge3J9CmNhdCgiTWVhbiBNUFIgYWNyb3NzIGFsbCB1c2VycyAoTUYpID0gIiwgbWVhbihyYW5rc19NRiksIlxuIikKY2F0KCJNZWRpYW4gTVBSIGFjcm9zcyBhbGwgdXNlcnMgKE1GKSA9ICIsIG1lZGlhbihyYW5rc19NRiksIlxuIikKYGBgCgpMb29rcyBsaWtlIHRoZSBtZWFuIGFuZCBtZWRpYW4gYXJlIG5vdCB0b28gZmFyIGFwYXJ0LiBMZXQncyBsb29rIGF0IHRoZSBoaXN0b2dyYW0gb2YgYWxsIHVzZXJzJyBQUgpgYGB7cn0KZ2dwbG90KGFzLmRhdGEuZnJhbWUocmFua3NfTUYpLCBhZXMocmFua3NfTUYsIGZpbGw9IHJhbmtzX01GID4gNTApKSArIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMSkgKyB4bGFiKCJJbmRpdmlkdWFsIFBlcmNlbnRpbGUgUmFua2luZyIpICsgeWxhYigiTnVtYmVyIG9mIHVzZXJzIikKYGBgCgoKYGBge3J9CmNhdCgiTnVtYmVyIG9mIHVzZXJzIHdpdGggYmV0dGVyIHRoYW4gcmFuZG9tIHByZWRpY3Rpb25zID0iLCBsZW5ndGgoc3Vic2V0KHJhbmtzX01GLHJhbmtzX01GPDUwKSksICJcbiIpCmNhdCgiTnVtYmVyIG9mIHVzZXJzIHdpdGggd29yc2UgdGhhbiByYW5kb20gcHJlZGljdGlvbnMgPSIsIGxlbmd0aChzdWJzZXQocmFua3NfTUYscmFua3NfTUY+PTUwKSksICJcbiIpCmBgYApMZXQncyBhbHNvIGxvb2sgYXQgdGhlIHNjYXR0ZXIgcGxvdCBiZXR3ZWVuIGluZGl2aWR1YWwgdXNlciBQUnMgYW5kIHRoZWlyIGZyZXF1ZW5jeSBvZiBwdXJjaGFzZQpgYGB7cn0KcHJlZF9NRiA9IGRhdGEuZnJhbWUodGVzdF9wYWRkZWRbLDFdLHRlc3RfcGFkZGVkWywyXSxyZWNvMikKdXNlcnNfTUYgPSBkYXRhLmZyYW1lKHVuaXF1ZShwcmVkX01GJHVzZXJfaWQpLHJhbmtzX01GKQp1c2VyX2NvdW50c190ZXN0JHJhbmtzX01GID0gdXNlcnNfTUYkcmFua3NfTUYKZ2dwbG90KHVzZXJfY291bnRzX3Rlc3QpICsgZ2VvbV9wb2ludChhZXMoeD10b3RhbCwgeT1yYW5rc19NRiwgY29sb3IgPSByYW5rc19NRiA+IDUwKSkgKyB4bGFiKCJUb3RhbCBwdXJjaGFzZXMiKSArIHlsYWIoIkluZGl2aWR1YWwgUGVyY2VudGlsZSBSYW5raW5nIikKYGBgCioqT2JzZXJ2YXRpb246KiogSXQgaXMgdmVyeSBldmlkZW50IHRoYXQgdGhlIHBlcmZvcm1hbmNlIG9mIE1GIGlzIHBvb3IgbWFpbmx5IGZvciB1c2VycyB3aG8gYXJlIGluZnJlcXVlbnQgYnV5ZXJzLCB3aGljaCB3YXMgbm90IHRoZSBjYXNlIGZvciBBTFMuIFRoaXMgaXMgZXhwZWN0ZWQgYmVjYXVzZSBBTFMgaXMgdHVuZWQgZm9yIGltcGxpY2l0IGZlZWRiYWNrIGRhdGEgd2hlcmUgYW4gaW5mcmVxdWVudCB1c2VyIGRvZXNuJ3QgY29ycmVzcG9uZCB0byBhIHVzZXIgd2hvICoiZGlzbGlrZXMiKiB0aG9zZSBwcm9kdWN0LiBXaGlsZSB0aGUgTUYgd291bGQgdGhpbmsgdGhhdCBpbmZyZXF1ZW50IHVzZXJzICJkaXNsaWtlIiB0aGF0IHByb2R1Y3QgYW5kIHdvdWxkIHRoZXJlZm9yZSBtYWtlIHdyb25nIHByZWRpY3Rpb25zIGFib3V0IHdoYXQgdGhleSBtaWdodCBidXkgaW4gdGhlIGZ1dHVyZS4gKipJbiBwYXJ0aWN1bGFyLCB0aGlzIHdvdWxkIG1lYW4gdGhhdCB0aGUgTUYgYWxnb3JpdGhtIHdvdWxkIE5PVCByZWNvbW1lbmQgcHJvZHVjdHMgd2hpY2ggYXJlIHNpbWlsYXIgdG8gdGhlIG9uZXMgdGhlIGluZnJlcXVlbnQgdXNlcnMgYWxyZWFkeSBib3VnaHQqKiAtLSB3aGljaCBpcyBub3QgY29ycmVjdAoKQWxtb3N0IGFsbCBvZiB0aGUgY2FzZXMgd2hlcmUgdGhlIHBlcmZvcm1hbmNlIGlzIHdvcnNlIHRoYW4gNTAlIGFyZSBmb3IgdXNlcnMgd2hvIGhhdmUgYm91Z2h0IGxlc3MgdGhhbiAyNSBwcm9kdWN0cyAodXBwZXIgbGVmdCBjb3JuZXIpLiBMaWtlIGRpc2N1c3NlZCBlYXJsaWVyLCBhIGJldHRlciBkb3duLXNhbXBsaW5nIHNjaGVtZSB3aGljaCByZXN1bHRzIGluIGEgbW9yZSBkZW5zZSBtYXRyaXggbWlnaHQgeWllbGQgYmV0dGVyIHBlcmZvcm1hbmNlcyB3aGVyZSB3ZSBjYW4gbGV2ZXJhZ2UgbW9yZSBmcm9tIGZyZXF1ZW50IChwb3dlcikgdXNlcnMKCmBgYHtyfQpjYXQoJyUgb2YgdXNlcnMgd2hvIGJvdWdodCA8IDI1IHByb2R1Y3RzIChvdmVyYWxsKSA9JywgbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCx0b3RhbDwyNSkpL25yb3codXNlcl9jb3VudHNfdGVzdCkqMTAwLCAiJVxuIikKY2F0KCclIG9mIHVzZXJzIHdobyBib3VnaHQgPCAyNSBwcm9kdWN0cyAod2l0aCBQUiA+IDUwJSkgPScsIG5yb3coc3Vic2V0KHVzZXJfY291bnRzX3Rlc3QsdG90YWw8MjUgJiByYW5rc19NRiA+IDUwKSkvbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCwgcmFua3NfTUYgPiA1MCkpKjEwMCwgIiVcbiIpCmBgYAoKCiMjIyBTdW1tYXJ5CgojIyMjIERhdGE6CisgTnVtYmVyIG9mIHVzZXJzOiAxMSwyNjMKKyBOdW1iZXIgb2YgcHJvZHVjdHM6IDMsMTM4CisgVHJhaW4gZGF0YSBwb2ludHM6IDc5LDMwNyAoNjglKSAKKyBUZXN0IGRhdGEgcG9pbnRzOiAzNywzMjggKDMyJSkKCiMjIyMgT3V0cHV0OgorIE1lYW4gUGVyY2VudGlsZSBSYW5rIHVzaW5nIFdlaWdodGVkIEFsdGVybmF0aW5nIExlYXN0IFNxdWFyZXMgbWV0aG9kIChIdSBldC4gYWwuKTogKioxMy40OSUqKiAoTWVkaWFuOiA3LjQxJSkKKyBNZWFuIFBlcmNlbnRpbGUgUmFuayB1c2luZyBNYXRyaXggRmFjdG9yaXphdGlvbjogKioyNy44MCUqKiAoTWVkaWFuOiAyNC43MSUpCgoKIyMjIERpc2N1c3Npb24KVGhlIGV4cGVyaW1lbnRzIHBlcmZvcm1lZCBoZXJlIGFyZSBleHBsb3JhdG9yeSBpbiBuYXR1cmUgYW5kIGRvIG5vdCByZWZsZWN0IHRoZSB0cnVlIHBvdGVudGlhbCBvZiB0aGVzZSBhbGdvcml0aG1zLiBUaGVyZSBpcyBhIGxvdCBvZiByb29tIGZvciBpbXByb3ZlbWVudDoKCjEuICoqRGF0YSBzZXQ6KiogV2UgdXNlZCBvbmx5IGEgc21hbGwgc3Vic2V0IG9mIHRoZSBkYXRhIChhYm91dCA1LjUlIG9mIHVzZXJzIGFuZCA2LjUlIG9mIHByb2R1Y3RzKS4gVGhlIGdvYWwgb2YgbXkgZXhwZXJpbWVudHMgd2VyZSB0byBtYWtlIHRoZW0gcnVubmFibGUgb24gYSBzaW5nbGUgY29tcHV0ZXIuIEJ5IGRlcGxveWluZyB0aGVzZSBhbGdvcml0aG1zIG9uIGEgc3BhcmsgY2x1c3RlciBvdmVyIHRoZSBlbnRpcmUgZGF0YSBjYW4gZ2l2ZSBtdWNoIGJldHRlciByZXN1bHRzLgoyLiAqKlNhbXBsaW5nOioqIExpa2UgZGVzY3JpYmVkIGFib3ZlLCB3ZSBjYW4gdXNlIGEgaHlicmlkIHNhbXBsaW5nIGFwcHJvYWNoIHNvIHRoYXQgZXZlbiB3aXRoIHRoZSBzbWFsbCBzdWJzZXQgb2YgZGF0YSwgd2UgY2FuIGdldCBhIGJldHRlciByZXByZXNlbnRhdGlvbiBvZiBkaWZmZXJlbnQgdHlwZXMgb2YgdXNlcnMgYW5kIHByb2R1Y3RzIGJhc2VkIG9uIHRoZWlyIGZyZXF1ZW5jeS4gVGhpcyBtaWdodCBoZWxwIGltcHJvdmUgdGhlIHJlc3VsdHMgZnVydGhlci4KMy4gKipQYXJhbWV0ZXIgdHVuaW5nOioqIEJldHRlci9tb3JlIHBhcmFtZXRlciB0dW5pbmcgY291bGQgaGF2ZSByZXN1bHRlZCBpbiBiZXR0ZXIgTVBSczoKICAgICsgVGhlIG51bWJlciBvZiBjb21iaW5hdGlvbnMgZ3JvdyBleHBvbmVudGlhbGx5IHdpdGggZWFjaCBuZXcgdmFsdWUgb2YgYSBoeXBlcnBhcmFtZXRlciB3aGljaCBhZmZlY3RzIHRoZSBjb21wdXRhdGlvbiB0aW1lIGZvciBjcm9zcy12YWxpZGF0aW9uLiBXZSBjb3VsZCBkbyBtb3JlIG9wdGltaXplZCBwYXJhbWV0ZXIgdHVuaW5nIHVzaW5nIGNvYXJzZSB0byBmaW5lIG1ldGhvZHMuCiAgICArIEZvciBBbHRlcm5hdGluZyBMZWFzdCBTcXVhcmVzIG1ldGhvZCwgd2UgZGlkbid0IGRvIHRyYWRpdGlvbmFsIGNyb3NzLXZhbGlkYXRpb24sIGJ1dCBhIHZhcmlhbnQgb2YgaXQgdXNpbmcgc21hbGxlciBkYXRhIHNldHMuIFRyYWRpdGlvbmFsIGNyb3NzLXZhbGlkYXRpb24gY291bGQgaGF2ZSB5aWVsZWQgYmV0dGVyIGh5cGVycGFyYW1ldGVyIGVzdGltYXRlcyBiZWNhdXNlIGl0IHdvcmtzIG9uIGxhcmdlciBzbGljZXMgb2YgdHJhaW4vdmFsaWRhdGlvbiBkYXRhLgogICAgKyBGb3IgTWF0cml4IEZhY3Rvcml6YXRpb24gbWV0aG9kLCB3ZSB1c2VkIHRoZSBidWlsdC1pbiBwYXJhbWV0ZXIgdHVuaW5nIHdoaWNoIGRvZXNuJ3Qgb3B0aW1pemUgdGhlIE1QUi4gV3JpdGluZyBvdXIgb3duIG1ldGhvZHMgZm9yIGRvaW5nIHRoYXQgbWlnaHQgaGF2ZSBnaXZlbiBiZXR0ZXIgaHlwZXJwYXJhbWV0ZXIgZXN0aW1hdGVzLiBXZSBjb3VsZCBhbHNvIHRyeSBtaW5pbWl6aW5nIHRoZSBMMSBsb3NzIGluc3RlYWQgb2YgTDIgZm9yIGJvdGggdGhlIGxhbWJkYSB2YWx1ZXMKNC4gKipBbGdvcml0aG1zOioqIFdlIGNvdWxkIGFsc28gbG9vayBhdCB0aGUgZGlmZmVyZW50IGZvcm11bGF0aW9ucyBvZiB0aGUgYWxnb3JpdGhtcy4gQW4gaW1wb3J0YW50IHZhcmlhdGlvbiBpbnZvbHZlcyBpbmNsdWRpbmcgdGhlIGJpYXMgdGVybXMgZm9yIGV2ZXJ5IHVzZXIgYW5kIHByb2R1Y3QuIFRoaXMgd291bGQgZW5jb2RlIGNlcnRhaW4gdXNlcnMnIHByZWZlcmVuY2UgdG8gYnV5IG1vcmUvZGl2ZXJzZSBzZXQgb2YgcHJvZHVjdHMgdnMgb3RoZXJzIGFuZCBjZXJ0YWluIHByb2R1Y3RzIGJlaW5nIGdlbmVyYWxseSBtb3JlIHBvcHVsYXIvaW1wb3J0YW50IHRoYW4gb3RoZXJzLiBUaGUgQUxTIGFsZ29yaXRobSBjYW4gYmUgZWFzaWx5IG1vZGlmaWVkIHRvIGludHJvZHVjZSB0aGVzZSBiaWFzZXMgKFtsaWtlIHN1Z2dlc3RlZCBoZXJlXShodHRwOi8vYWN0aXZpc2lvbmdhbWVzY2llbmNlLmdpdGh1Yi5pby8yMDE2LzAxLzExL0ltcGxpY2l0LVJlY29tbWVuZGVyLVN5c3RlbXMtQmlhc2VkLU1hdHJpeC1GYWN0b3JpemF0aW9uLykpLiBMaWtld2lzZSwgSm9obnNvbidzIGFwcHJvYWNoIHVzaW5nIFtMb2dpc3RpYyBNYXRyaXggRmFjdG9yaXphdGlvbl0oaHR0cHM6Ly9zdGFuZm9yZC5lZHUvfnJlemFiL25pcHMyMDE0d29ya3Nob3Avc3VibWl0cy9sb2dtYXQucGRmKSBjYW4gYWxzbyBiZSB1c2VkLiBUaGF0IHNhaWQsIEkgYmVsaWV2ZSB0aGUgYmlnZ2VzdCBnYWlucyB3aWxsIGxpa2VseSBjb21lIGZyb20gdXNpbmcgbW9yZSBkYXRhLg==