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:
- recommenderlab package
- 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:
- All product id’s and user id’s are there in both train and test sets
- 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:
- Randomly partition “data” table into “train” and “test” tables as per the required split ratio
- 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 %
LS0tCnRpdGxlOiAiSW5zdGFjYXJ0IHJlY29tbWVuZGF0aW9uIHByb2JsZW0iCm91dHB1dDoKICBodG1sX25vdGVib29rOiBkZWZhdWx0CiAgaHRtbF9kb2N1bWVudDogZGVmYXVsdAogIHBkZl9kb2N1bWVudDogZGVmYXVsdAotLS0KCiMjIyBQcm9ibGVtIGRlZmluaXRpb24KClRoZSBbZGF0YSBzZXRdKGh0dHBzOi8vd3d3Lmluc3RhY2FydC5jb20vZGF0YXNldHMvZ3JvY2VyeS1zaG9wcGluZy0yMDE3KSBkZXNjcmliZXMgY3VzdG9tZXIgb3JkZXJzIG92ZXIgdGltZS4gVGhlIGdvYWwgb2YgdGhlIGV4cGVyaW1lbnQgaXMgdG8gcHJlZGljdCB3aGljaCBwcm9kdWN0cyB1c2VycyB3aWxsIGJ1eSBuZXh0LCBiYXNlZCBvbiB0aGVpciBwdXJjaGFzZSBoaXN0b3J5LiBGb3IgY29udmVuaWVuY2UsIHRoZSBvcmlnaW5hbCBrYWdnbGUgcHJvYmxlbSBoYXMgYmVlbiBzbGlnaHRseSByZWZvcm11bGF0ZWQ6IHdlIHNob3VsZCBvbmx5IGNvbnNpZGVyIFw8dXNlcmlkLCBwcm9kdWN0aWRcPiBwYWlycyBhbmQgZGlzY2FyZCBhbnkgb3RoZXIgYXNzb2NpYXRlZCBmZWF0dXJlcyBsaWtlIGRheSBvZiB3ZWVrIGFuZCBob3VyIG9mIGRheSBvZiB0aGUgcHVyY2hhc2UuIExldCdzIGdldCBzdGFydGVkIQoKVGhlIHR3byBtYWluIHBhY2thZ2VzIHRoYXQgd2Ugd2lsbCBiZSB1c2luZyBhcmUgInJlY29tbWVuZGVybGFiIiBhbmQgInJlY29zeXN0ZW0iLiBUaGVzZSBwYWNrYWdlcyBwcm92aWRlIGRpZmZlcmVudCBhbGdvcml0aG1zIGZvciBjb2xsYWJvcmF0aW9uIGZpbHRlcmluZyAobWF0cml4IGNvbXBsZXRpb24pLiBNb3JlIGluZm8gY2FuIGJlIGZvdW5kIGhlcmU6CgoxLiBbcmVjb21tZW5kZXJsYWIgcGFja2FnZV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3JlY29tbWVuZGVybGFiL2luZGV4Lmh0bWwpCjIuIFtyZWNvc3lzdGVtIHBhY2thZ2VdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9yZWNvc3lzdGVtL2luZGV4Lmh0bWwpCgo8YnI+CgojIyMjIExvYWQvaW5zdGFsbCBsaWJyYXJpZXMKTGV0J3MgbG9hZCB0aGUgcmVxdWlyZWQgbGlicmFyaWVzIGFuZCBzb3VyY2UgbWV0aG9kcyBmaXJzdC4gCgoqKk5vdGU6KioKV2Ugd2lsbCB1c2UgdGhlIGxhdGVzdCAoYmV0YSkgdmVyc2lvbiBvZiB0aGUgInJlY29tbWVuZGVybGFiIiBwYWNrYWdlLiBZb3Ugc2hvdWxkIGRpcmVjdGx5IGluc3RhbGwgaXQgZnJvbSBnaXRodWIgYW5kIHNob3VsZCBOT1QgdXNlICJpbnN0YWxsLnBhY2thZ2UiIChzZWUgY29tbWVudGVkIGNvZGUgYmVsb3cpCmBgYHtyfQpyZXF1aXJlKGRwbHlyKQpyZXF1aXJlKGRhdGEudGFibGUpCgojIGxpYnJhcnkoZGV2dG9vbHMpCiMgCiMgaWYoInJlY29tbWVuZGVybGFiIiAlaW4lIHJvd25hbWVzKGluc3RhbGxlZC5wYWNrYWdlcygpKSA9PSBGQUxTRSl7CiMgICBpbnN0YWxsX2dpdGh1YigibWhhaHNsZXIvcmVjb21tZW5kZXJsYWIiKQojIH0KCnJlcXVpcmUocmVjb21tZW5kZXJsYWIpCnJlcXVpcmUocmVjb3N5c3RlbSkKcmVxdWlyZShnZ3Bsb3QyKQpyZXF1aXJlKGdyaWQpCnJlcXVpcmUoZ3JpZEV4dHJhKQoKc291cmNlKCJjcm9zc3ZhbGlkYXRlQUxTLlIiKQpzb3VyY2UoInBhcnRpdGlvbkRhdGEuUiIpCnNvdXJjZSgiY3Jvc3N2YWxpZGF0ZVVCQ0YuUiIpCnNvdXJjZSgiZXZhbHVhdGVNb2RlbDIuUiIpCnNvdXJjZSgicHJlcGFyZVRyYWluVGVzdERhdGEuUiIpCnNvdXJjZSgicGFkVGVzdERhdGEuUiIpCmBgYAo8YnI+CgojIyMjIFJlYWQgZGF0YQoKTmV4dCwgbGV0J3MgcmVhZCB0aGUgZGF0YSBmaWxlcyAoc3RvcmVkIGxvY2FsbHkgaW4gImluc3RhY2FydF8yMDE3XzA1XzAxIiBkaXJlY3RvcnkpCmBgYHtyfQphaXNsZXMgPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvYWlzbGVzLmNzdicpICAgICAgICAgICAgICMgMTM0IGFpc2xlcwpkZXBhcnRtZW50cyA9IGZyZWFkKCdpbnN0YWNhcnRfMjAxN18wNV8wMS9kZXBhcnRtZW50cy5jc3YnKSAgICMgMjEgZGVwYXJ0bWVudHMKcHJvZHVjdHMgPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvcHJvZHVjdHMuY3N2JykgICAgICAgICAjIH41MEsgcHJvZHVjdHMKb3JkZXJfcHJvZHVjdHNfcHJpb3IgPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvb3JkZXJfcHJvZHVjdHNfX3ByaW9yLmNzdicsa2V5PWMoIm9yZGVyX2lkIikpICAgICMgfjMyTSBpbmRpdmlkdWFsIHByb2R1Y3RzIG9yZGVyZWQKb3JkZXJfcHJvZHVjdHNfdHJhaW4gPSBmcmVhZCgnaW5zdGFjYXJ0XzIwMTdfMDVfMDEvb3JkZXJfcHJvZHVjdHNfX3RyYWluLmNzdicsa2V5PWMoIm9yZGVyX2lkIikpICAgICMgfjEuM00gaW5kaXZpZHVhbCBwcm9kdWN0cyBvcmRlcmVkIChpZ25vcmUpCm9yZGVycyA9IGZyZWFkKCdpbnN0YWNhcnRfMjAxN18wNV8wMS9vcmRlcnMuY3N2JyxrZXk9Yygib3JkZXJfaWQiKSkKYGBgCjxicj4KCiMjIyMgRGF0YSBwcmVwcm9jZXNzaW5nCiogV2Ugd2lsbCBjb25zaWRlciBvbmx5IHRoZSBvcmRlcnMgZGF0YSBtYXJrZWQgInByaW9yIi4gCiogVGhlIGRhdGEgc2V0IGlzIG5vcm1hbGl6ZWQsIHNvIHRvIG9idGFpbiBcPHVzZXJpZCwgcHJvZHVjdGlkPiBwYWlycywgd2UgbmVlZCB0byBqb2luICJvcmRlcnNfcHJpb3IiIGFuZCAib3JkZXJfcHJvZHVjdHNfcHJpb3IiIHRhYmxlcyBvbiB0aGUga2V5ICJvcmRlcl9pZCIuIAoqIFdlIHdpbGwgdGhlbiBzb3J0IHRoZSByZXN1bHRpbmcgdGFibGUgKCJvcmRlcl9hbGwiKSBvbiAidXNlcl9pZCIgYW5kICJwcm9kdWN0X2lkIiBrZXlzIAoqIEZyb20gdGhpcywgd2UnbGwgb2J0YWluICJkYXRhX2luZCIgd2hpY2ggaXMgYSBsaXN0IG9mIGluZGl2aWR1YWwgXDx1c2VyX2lkLCBwcm9kdWN0X2lkXD4gcGFpcnMKKiBGcm9tICJkYXRhX2luZCIsIHdlIGNhbiBhY2N1bXVsYXRlIHNpbWlsYXIgXDx1c2VyX2lkLCBwcm9kdWN0X2lkXD4gcGFpcnMgYW5kIGZvcm0gImRhdGEiIHRhYmxlIHdoaWNoIGNvbnRhaW5zIFw8dXNlcl9pZCwgcHJvZHVjdF9pZCwgY291bnRcPiB0cmlwbGVzCmBgYHtyfQpvcmRlcnNfcHJpb3IgPSBzdWJzZXQob3JkZXJzLGV2YWxfc2V0PT0icHJpb3IiKSAgICAgICAgICMgfjMuMk0gInByaW9yIiBvcmRlcnMKb3JkZXJzX2FsbCA9IG1lcmdlKG9yZGVyc19wcmlvcixvcmRlcl9wcm9kdWN0c19wcmlvciwgYnk9Im9yZGVyX2lkIikKCm9yZGVyc19hbGwgPSBvcmRlcnNfYWxsW29yZGVyKG9yZGVyc19hbGwkdXNlcl9pZCxvcmRlcnNfYWxsJHByb2R1Y3RfaWQpLF0KCmRhdGFfaW5kID0gb3JkZXJzX2FsbFssYygidXNlcl9pZCIsICJwcm9kdWN0X2lkIildCgpzZXRrZXkoZGF0YV9pbmQsdXNlcl9pZCxwcm9kdWN0X2lkKQoKZGF0YSA9IGFnZ3JlZ2F0ZShjYmluZChjb3VudCA9IHByb2R1Y3RfaWQpIH4gcHJvZHVjdF9pZCt1c2VyX2lkLCBkYXRhPWRhdGFfaW5kLCBGVU4gPSBsZW5ndGgpCmRhdGEgPSBzZXREVChkYXRhWyxjKDIsMSwzKV0pCmBgYAo8YnI+CgojIyMjIEdlbmVyYXRpbmcgdHJhaW4vdGVzdCBkYXRhCgpUaGUgZGF0YSBzZXQgaXMgdG9vIGJpZyB0byB3b3JrIHdpdGggb24gYSBzdGFuZGFsb25lIGNvbXB1dGVyLCBzbyB3ZSBhcmUgZ29pbmcgdG8gZG93bnNhbXBsZSBpdDsgYWx0aG91Z2ggaWRlYWxseSwgd2UgY2FuIGhvcGUgdG8gZ2V0IGJlc3QgcGVyZm9ybWFuY2UgdXNpbmcgZnVsbCBkYXRhLiBMZXQncyBzZWUgaG93IHRoZSBkaXN0cmlidXRpb24gb2YgdG90YWwgcHJvZHVjdCBwdXJjaGFzZXMgYWNyb3NzIGFsbCB1c2VycyBsb29rcyBsaWtlCgpgYGB7cn0KcHJvZHVjdF9jb3VudHMgPSBhZ2dyZWdhdGUoY2JpbmQodG90YWwgPSBjb3VudCkgfiBwcm9kdWN0X2lkLCBkYXRhPWRhdGFbLDI6M10sIEZVTiA9IHN1bSkKZ2dwbG90KHByb2R1Y3RfY291bnRzLCBhZXModG90YWwpKSArIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMTAwMCkgKyB4bGFiKCJudW1iZXIgb2YgdGltZXMgYm91Z2h0IikKYGBgCgpBcyBleHBlY3RlZCwgdGhpcyBsb29rcyBoaWdobHkgbm9uLXVuaWZvcm0uIExldCdzIGxvb2sgYXQgdHdvIGRpZmZlcmVudCByYW5nZXMgZm9yIGJldHRlciB1bmRlcnN0YW5kaW5nOiBwcm9kdWN0cyB0aGF0IGFyZSBib3VnaHQgbGVzcyB0aGFuIDIwMDAgdGltZXMgYW5kIG9uZXMgYm91Z2h0IGJldHdlZW4gMjAwMCB0byAyMDAwMCB0aW1lcwoKYGBge3J9CnAxID0gZ2dwbG90KHN1YnNldChwcm9kdWN0X2NvdW50cyx0b3RhbDwyMDAwKSwgYWVzKHRvdGFsKSkgKyBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDEwKSArIHhsYWIoIm51bWJlciBvZiB0aW1lcyBib3VnaHQiKQpwMiA9IGdncGxvdChzdWJzZXQocHJvZHVjdF9jb3VudHMsdG90YWw+MjAwMCAmIHRvdGFsIDwgMjAwMDApLCBhZXModG90YWwpKSArIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMTAwKSArIHhsYWIoIm51bWJlciBvZiB0aW1lcyBib3VnaHQiKSArIHhsaW0oMTAwMCwyMDAwMCkKZ3JpZC5hcnJhbmdlKHAxLCBwMiwgbmNvbCA9IDIpCmBgYAoKU2ltaWxhcmx5LCBsZXQncyBwbG90IHRoZSBudW1iZXIgb2YgZnJlcXVlbmN5IG9mIGRpZmZlcmVudCB1c2VycyBpbiB0aGUgZGF0YToKCmBgYHtyfQp1c2VyX2NvdW50cyA9IGFnZ3JlZ2F0ZShjYmluZCh0b3RhbCA9IGNvdW50KSB+IHVzZXJfaWQsIGRhdGE9ZGF0YVssYygxOjMpXSwgRlVOID0gc3VtKQpnZ3Bsb3QodXNlcl9jb3VudHMsIGFlcyh0b3RhbCkpICsgZ2VvbV9oaXN0b2dyYW0oYmlud2lkdGggPSAxMCkgKyB4bGFiKCJUb3RhbCBudW1iZXIgb2YgcHJvZHVjdHMgYm91Z2h0IGJ5IGEgdXNlciIpCmBgYApBcyB3ZSBjYW4gc2VlIHRoYXQgdGhlIG51bWJlciBvZiBwcm9kdWN0cyBhbmQgdXNlcnMgZGVjcmVhc2Ugc2hhcnBseSB3aXRoIHRoZWlyIGZyZXF1ZW5jeS4gSW4gb3RoZXIgd29yZHM6CgoqIE1vc3QgcHJvZHVjdHMgd2VyZSBib3VnaHQgcmFyZWx5LCB3aGlsZSBvbmx5IGEgdmVyeSBmZXcgcHJvZHVjdHMgd2VyZSBmcmVxdWVudGx5IGJvdWdodAoqIE1vc3QgdXNlcnMgYm91Z2h0IHZlcnkgZmV3IHByb2R1Y3RzLCB3aGlsZSB2ZXJ5IGZldyB1c2VycyBib3VnaHQgYSBsb3Qgb2YgcHJvZHVjdHMKCkdpdmVuIHRoaXMsIHRoZXJlIGFyZSBkaWZmZXJlbnQgd2F5cyBpbiB3aGljaCB3ZSBjYW4gZG93bi1zYW1wbGUgdGhlIGRhdGE6CgoqICoqU2NoZW1lIDEgKFVuaWZvcm0gc2FtcGxpbmcpOioqIFJhbmRvbWx5IHBpY2sgKlAqIHByb2R1Y3RzIGFuZCAqVSogdXNlcnMKICAgICsgKlByb3M6KiBEYXRhIGRpc3RyaWJ1dGlvbnMgb2YgcG9wdWxhciB2cy4gcmFyZWx5LWJvdWdodCBwcm9kdWN0cyBhbmQgZnJlcXVlbnQgdnMuIGluZnJlcXVlbnQgdXNlcnMgYXJlIHByZXNlcnZlZAogICAgKyAqQ29uczoqIFdpdGggc21hbGwgKlAqIGFuZCAqVSosIHdlIG1pZ2h0IGVuZCB1cCB3aXRoIG1vc3RseSByYXJlbHkgYm91Z2h0IHByb2R1Y3RzIGFuZCBpbmZyZXF1ZW50IHVzZXJzLCBiZWNhdXNlIHRob3NlIGFyZSB0aGUgb25lcyB3aXRoIHZhc3QgbWFqb3JpdHkuCiAgICAKKiAqKlNjaGVtZSAyIChXZWlnaHRlZCBzYW1wbGluZyk6KiogUGljayAqUCogcHJvZHVjdHMgYW5kICpVKiB1c2VycyBiYXNlZCBvbiB0aGVpciBmcmVxdWVuY3kgb2YgcHVyY2hhc2UKICAgICsgKlByb3M6KiBXZSB3aWxsIGdldCBtb3JlIGZyZXF1ZW50IHVzZXJzIGFuZCBwb3B1bGFyIHByb2R1Y3RzLCBzbyB0aGUgZGF0YSBtYXRyaXggd2lsbCBiZSBtb3JlIGRlbnNlLCB0aHVzIGluY3JlYXNpbmcgdGhlIHByZWRpY3RpdmUgcG93ZXIgb2YgdGhlIGFsZ29yaXRobQogICAgKyAqQ29uczoqIE9yaWdpbmFsIGRhdGEgZGlzdHJpYnV0aW9ucyBhcmUgbm90IHByZXNlcnZlZC4gU28gcGVyZm9ybWFuY2UgbWlnaHQgYmUgcG9vciByYXJlbHktYm91Z2h0IHByb2R1Y3RzIGFuZCBpbmZyZXF1ZW50IHVzZXJzCiAgICAKKiAqKlNjaGVtZSAzIChIeWJyaWQgc2FtcGxpbmcpOioqIFBpY2sgKlAqIHByb2R1Y3RzIGFuZCAqVSogdXNlcnMgYmFzZWQgb24gc29tZSBjb21iaW5hdGlvbiBvZiBTY2hlbWUgMSBhbmQgMiBhYm92ZS4gRm9yIGV4YW1wbGUsIHdlIGNhbiBkaXZpZGUgdGhlIHByb2R1Y3RzIGFuZCB1c2VycyBpbnRvICpOKiBwZXJjZW50aWxlcyBiYXNlZCBvbiB0aGVpciBmcmVxdWVuY3kgYW5kIHRoZW4gdW5pZm9ybWx5IHNhbXBsZSBmcm9tIGVhY2ggYmluCiAgICArICpQcm9zOiogV2Ugd2lsbCBnZXQgYSBnb29kIG1peCBvZiBhbGwgdHlwZXMgb2YgdXNlcnMgYW5kIHByb2R1Y3RzLCBzbyBvdmVyYWxsIHByZWRpY3RpdmUgcG93ZXIgc2hvdWxkIGJlIGJldHRlcgogICAgKyAqQ29uczoqIE9yaWdpbmFsIGRhdGEgZGlzdHJpYnV0aW9ucyBhcmUgbm90IHByZXNlcnZlZAogIAoqKkZvciBub3csIHdlJ2xsIGdvIHdpdGggU2NoZW1lIDEsIGluIGZhdm9yIG9mIHByZXNlcnZpbmcgdGhlIG9yaWdpbmFsIGRhdGEgZGlzdHJpYnV0aW9uIHdpdGggKlAqPTUwMDAgYW5kICpVKj0xNTAwMCoqIAoKYGBge3J9CmRhdGFfb3JpZyA9IGRhdGEgIyBzYXZlIHRoZSBvcmlnaW5hbCBkYXRhCgp1c2VycyA9IHVuaXF1ZShkYXRhJHVzZXJfaWQpCnVzZXJfc2FtcGxlcyA9IHVzZXJzW3NhbXBsZShsZW5ndGgodW5pcXVlKGRhdGEkdXNlcl9pZCkpLCAxNTAwMCwgcmVwbGFjZSA9IEZBTFNFKV0KZGF0YSA9IHN1YnNldChkYXRhLHVzZXJfaWQgJWluJSB1c2VyX3NhbXBsZXMpCgpwcm9kdWN0cyA9IHVuaXF1ZShkYXRhJHByb2R1Y3RfaWQpCnByb2R1Y3Rfc2FtcGxlcyA9IHByb2R1Y3RzW3NhbXBsZShsZW5ndGgodW5pcXVlKGRhdGEkcHJvZHVjdF9pZCkpLCA1MDAwLCByZXBsYWNlID0gRkFMU0UpXQpkYXRhID0gc3Vic2V0KGRhdGEscHJvZHVjdF9pZCAlaW4lIHByb2R1Y3Rfc2FtcGxlcykKCmBgYAoKKiBOb3csIHBhcnRpdGlvbiB0aGUgZGF0YSBpbnRvIHRyYWluL3Rlc3QgKDcwLzMwKSBtYWtpbmcgc3VyZSB0aGF0IHRoZSBmb2xsb3dpbmcgY29uZGl0aW9ucyBhcmUgbWV0OgogICAgMS4gQWxsIHByb2R1Y3QgaWQncyBhbmQgdXNlciBpZCdzIGFyZSB0aGVyZSBpbiBib3RoIHRyYWluIGFuZCB0ZXN0IHNldHMKICAgIDIuIEEgcGFpciBcPHVzZXJpZCwgcHJvZHVjdGlkXD4gY2FuIGVpdGhlciBhcHBlYXIgaW4gdHJhaW4gb3IgdGVzdCBzZXQsIGJ1dCBub3QgYm90aAoqIEluICJwYXJ0aXRpb25EYXRhIiBtZXRob2QsIHdlIHBhcnRpdGlvbiB0aGUgZGF0YSBpbiB0d28gc3RlcHM6CiAgICAxLiBSYW5kb21seSBwYXJ0aXRpb24gImRhdGEiIHRhYmxlIGludG8gInRyYWluIiBhbmQgInRlc3QiIHRhYmxlcyBhcyBwZXIgdGhlIHJlcXVpcmVkIHNwbGl0IHJhdGlvCiAgICAyLiBSZW1vdmUgdXNlcnMvcHJvZHVjdHMgdGhhdCBhcmUgdGhlcmUgaW4gb25lIHNldCBhbmQgbm90IGluIGFub3RoZXIKICAgIAogICAgKipOb3RlOioqIFRoaXMgaXMgbm90IHRoZSBiZXN0IHdheSB0byBzcGxpdCB0aGUgZGF0YSwgYW5kIGluY3VycyBzb21lIGxvc3MuIEEgYmV0dGVyIGFwcHJvYWNoIHdvdWxkIGJlIHRvIHBhcnRpdGlvbiBpdCBiYXNlZCBvbiB0aGUgam9pbnQgZGlzdHJpYnV0aW9uIG9mIHVzZXJzIGFuZCBwcm9kdWN0cyBpbiAiZGF0YSIKCmBgYHtyfQpkYXRhX3BhcnRzID0gcGFydGl0aW9uRGF0YShkYXRhID0gZGF0YSwgZnJhY0RhdGEgPSAxLCBmcmFjVHJhaW4gPSAwLjcpCnRyYWluID0gZGF0YV9wYXJ0cyR0cmFpbgp0ZXN0ID0gZGF0YV9wYXJ0cyR0ZXN0CmBgYAoKT2ssIG5vdyB3ZSBzaG91bGQgaGF2ZSBhIHRyYWluL3Rlc3Qgc3BsaXQgd2hlcmUgYm90aCBjb25kaXRpb25zIDEgJiAyIGFyZSBzYXRpc2ZpZWQuIExldCdzIGNoZWNrIHNwbGl0IHJhdGlvIGFnYWluCmBgYHtyfQpjYXQoIlRyYWluICUgPSAiLCBucm93KHRyYWluKS8obnJvdyh0cmFpbikgKyBucm93KHRlc3QpKSoxMDAsICJhbmQgdGVzdCAlID0gIiwgbnJvdyh0ZXN0KS8obnJvdyh0cmFpbikgKyBucm93KHRlc3QpKSoxMDAsICJcbiIpCmNhdCgibnJvd3MgdHJhaW4gPSAiLCBucm93KHRyYWluKSwgIlxuIikKY2F0KCJucm93cyB0ZXN0ID0gIiwgbnJvdyh0ZXN0KSwgIlxuIikKY2F0KCJOdW1iZXIgb2YgdW5pcXVlIHVzZXJfaWQncyA9ICIsIGxlbmd0aCh1bmlxdWUodHJhaW4kdXNlcl9pZCkpLCAiXG4iKQpjYXQoIk51bWJlciBvZiB1bmlxdWUgcHJvZHVjdF9pZCdzID0gIiwgbGVuZ3RoKHVuaXF1ZSh0cmFpbiRwcm9kdWN0X2lkKSksICJcbiIpCmBgYAoKQWxzbywgbGV0J3MgcXVpY2tseSBjb21wYXJlIHRoZSBzcGFyc2l0eSBvZiB0aGUgdHJhaW4vdGVzdCBkYXRhIHcuci50LiBvcmlnaW5hbCBkYXRhCmBgYHtyfQpjYXQoIlNwYXJzaXR5IGluIG9yaWdpbmFsIGRhdGEgPSAiLCAxMDAgLSBucm93KGRhdGFfb3JpZykvKGFzLmRvdWJsZShsZW5ndGgodW5pcXVlKGRhdGFfb3JpZyRwcm9kdWN0X2lkKSkpKmFzLmRvdWJsZShsZW5ndGgodW5pcXVlKGRhdGFfb3JpZyR1c2VyX2lkKSkpKSoxMDAsICIlXG4iKQpjYXQoIlNwYXJzaXR5IGluIHRyYWluIGRhdGEgPSAiLCAxMDAgLSBucm93KHRyYWluKS8oYXMuZG91YmxlKGxlbmd0aCh1bmlxdWUodHJhaW4kcHJvZHVjdF9pZCkpKSphcy5kb3VibGUobGVuZ3RoKHVuaXF1ZSh0cmFpbiR1c2VyX2lkKSkpKSoxMDAsICIlXG4iKQpjYXQoIlNwYXJzaXR5IGluIHRlc3QgZGF0YSA9ICIsIDEwMCAtIG5yb3codGVzdCkvKGFzLmRvdWJsZShsZW5ndGgodW5pcXVlKHRlc3QkcHJvZHVjdF9pZCkpKSphcy5kb3VibGUobGVuZ3RoKHVuaXF1ZSh0ZXN0JHVzZXJfaWQpKSkpKjEwMCwgIiVcbiIpCmBgYApTbyBpdCBpcyByb3VnaGx5IHRoZSBzYW1lLCB3aGljaCBpcyB3aGF0IHdlIGV4cGVjdCB3aXRoIHVuaWZvcm0gc2FtcGxpbmcgYXBwcm9hY2gKCjxicj4KCiMjIyMgRXhwbG9yaW5nIFdlaWdodGVkIEFsdGVybmF0aW5nIExlYXN0IFNxdWFyZSBhcHByb2FjaCAodmlhICJyZWNvbW1lbmRlcmxhYiIgcGFja2FnZSkKVGhlICJyZWNvbW1lbmRlcmxhYiIgcGFja2FnZSBpbXBsZW1lbnRzIHRoZSBBTFMgKGFsdGVybmF0aW5nIGxlYXN0IHNxdWFyZXMpIG9wdGltaXphdGlvbiBhcHByb2FjaCBkZXNjcmliZWQgaW4gW0h1IGV0LiBhbC4ncyBwYXBlcl0oaHR0cDovL3lpZmFuaHUubmV0L1BVQi9jZi5wZGYpCgoqKk5vdGUgMToqKiBUaGUgcGFja2FnZSAqaGFzKiBhIHJvdXRpbmUgdG8gdHVuZSBkaWZmZXJlbnQgcGFyYW1ldGVyczogdGhlIHJlZ3VsYXJpemF0aW9uIHBhcmFtZXRlciwgbGFtYmRhIGFuZCBsZW5ndGggb2YgdGhlIGxhdGVudCBmYWN0b3JzLCAqZCouIEJ1dCB0aGUgdHVuaW5nIGlzIHBlcmZvcm1lZCB3aGlsZSBvcHRpbWl6aW5nIGZvciBNU0UgKG1lYW4gc3F1YXJlZCBlcnJvciksIE1BRSAobWVhbiBhYnNvbHV0ZSBlcnJvcikgb3IgUk1TRSAocm9vdCBtZWFuIHNxdWFyZWQgZXJyb3IpLiBIb3dldmVyLCBzaW5jZSB3ZSBhcmUgaW50ZXJlc3RlZCBpbiBtaW5pbWl6aW5nIHRoZSBNUFIgKG1lYW4gcGVyY2VudGlsZSByYW5raW5nKSwgd2UnbGwgdXNlIG91ciBvd24gbWV0aG9kIGZvciB0dW5pbmcgdGhlc2UgcGFyYW1ldGVycwoKKipOb3RlIDI6KiogSWRlYWxseSwgZm9yIGNyb3NzLXZhbGlkYXRpb24sIHdlIHNob3VsZCBwYXJ0aXRpb24gdGhlIHRyYWluaW5nIGRhdGEgaW50byAqZiogZm9sZHMgYW5kIHVzZSBsZWF2ZS1vbmUtZm9sZC1vdXQgdGVjaG5pcXVlIHRvIHR1bmUgdGhlIHBhcmFtZXRlcnMuIEhvd2V2ZXIsIGluIGZhdm9yIG9mIHR1bmluZyB0aGUgcGFyYW1ldGVycyBmYXN0ZXIsIHdlIHdpbGwgcmVzYW1wbGUgdGhlICJ0cmFpbiIgZGF0YSBtdWx0aXBsZSB0aW1lcyB0byBnZW5lcmF0ZSB0cmFpbmluZy92YWxpZGF0aW9uIHNldHMgYW5kIHNpbXBseSBhdmVyYWdlIHRoZSBNUFIgb3ZlciBkaWZmZXJlbnQgcnVucyAoZm9sZHMpIGZvciBlYWNoIHNldCBvZiBwYXJhbWV0ZXJzCgpgYGB7cn0Kc291cmNlKCJldmFsdWF0ZU1vZGVsLlIiKQpzb3VyY2UoImNyb3NzdmFsaWRhdGVBTFMuUiIpCmxhbWJkYSA9IGMoMC4wMSwxLDEwKQpuRmFjdG9ycyA9IGMoMTAsMTAwLDIwMCkKYWxwaGEgPSBjKDEsIDEwLCAxMDApCnJhbmtzX0FMUyA9IGNyb3NzdmFsaWRhdGVBTFMoZGF0YSA9IHRyYWluLCBsYW1iZGEgPSBsYW1iZGEsIG5GYWN0b3JzID0gbkZhY3RvcnMsIGFscGhhID0gYWxwaGEsIGZyYWNEYXRhID0gMC4yLCBjdkZvbGRzID0gNCwgbkl0ZXIgPSAxMCwgZnJhY1RyYWluID0gMC43KQpgYGAKCgpgYGB7cn0KcmFua3NfQUxTCmBgYAoKYGBge3J9CmxhbWJkYV9iZXN0ID0gbGFtYmRhW3doaWNoKHJhbmtzX0FMUyA9PSBtaW4ocmFua3NfQUxTKSwgYXJyLmluZCA9IFRSVUUpWzFdXQpuRmFjdG9yc19iZXN0ID0gbkZhY3RvcnNbd2hpY2gocmFua3NfQUxTID09IG1pbihyYW5rc19BTFMpLCBhcnIuaW5kID0gVFJVRSlbMl1dCmFscGhhX2Jlc3QgPSBuRmFjdG9yc1t3aGljaChyYW5rc19BTFMgPT0gbWluKHJhbmtzX0FMUyksIGFyci5pbmQgPSBUUlVFKVszXV0KCmNhdCgiYmVzdCBsYW1iZGEgdmFsdWUgPSIsIGxhbWJkYV9iZXN0LCAiYW5kIGJlc3QgZCB2YWx1ZSA9IiwgbkZhY3RvcnNfYmVzdCwgImFuZCBiZXN0IGFscGhhIHZhbHVlID0iLCBhbHBoYV9iZXN0LCAiXG4iKQpgYGAKCk5vdyB0aGF0IHdlIGhhdmUgdGhlIG9wdGltYWwgcGFyYW1ldGVycywgbGV0cyB0cmFpbiBvbiBmdWxsIHRyYWluaW5nIGRhdGEgdXNpbmcgdGhlIG9wdGltYWwgcGFyYW1ldGVyIHZhbHVlcyBjb21wdXRlZCBhYm92ZSBhbmQgY29tcHV0ZS9ldmFsdWF0ZSB0aGUgcHJlZGljdGlvbnMuIE5vdGUgdGhhdCwgc2luY2Ugd2UgZG93bnNhbXBsZWQgdGhlIGRhdGEsIHBhZGRpbmcgd2l0aCAxMDAgcmFuZG9tIHNhbXBsZXMgaW5zdGVhZCBvZiAxMDAwIHNob3VsZCBiZSBzdWZmaWNpZW50CgpgYGB7cn0KUiA9IGFzKHRyYWluLCAicmVhbFJhdGluZ01hdHJpeCIpCnIgPSBSZWNvbW1lbmRlcihSLCBtZXRob2QgPSAiQUxTX2ltcGxpY2l0IiwgcGFyYW1ldGVyID0gbGlzdChuX2ZhY3RvcnM9bkZhY3RvcnNfYmVzdCwgbGFtYmRhPWxhbWJkYV9iZXN0LCBhbHBoYT1hbHBoYV9iZXN0LCBuX2l0ZXJhdGlvbnM9MjAsIHNlZWQgPSBOVUxMLCB2ZXJib3NlID0gVFJVRSkpIApyZWNvID0gcHJlZGljdChyLCBSLCB0eXBlID0gInJhdGluZ3MiKQpyYW5rX2ZpbmFsX3JlY29sYWIgPSBldmFsdWF0ZU1vZGVsKHJlY28gPSByZWNvLCB0ZXN0ID0gdGVzdCwgcGFkID0gMTAwKQpgYGAKCmBgYHtyfQpjYXQoIk1lYW4gTVBSIGFjcm9zcyBhbGwgdXNlcnMgPSAiLCBtZWFuKHJhbmtfZmluYWxfcmVjb2xhYiksIlxuIikKY2F0KCJNZWRpYW4gTVBSIGFjcm9zcyBhbGwgdXNlcnMgPSAiLCBtZWRpYW4ocmFua19maW5hbF9yZWNvbGFiKSwiXG4iKQpgYGAKTGV0J3MgYWxzbyBsb29rIGF0IHRoZSBmdWxsIGhpc3RvZ3JhbSBvZiBpbmRpdmlkdWFsIFBSJ3MKYGBge3J9CmdncGxvdChhcy5kYXRhLmZyYW1lKHJhbmtfZmluYWxfcmVjb2xhYiksIGFlcyhyYW5rX2ZpbmFsX3JlY29sYWIsIGZpbGw9IHJhbmtfZmluYWxfcmVjb2xhYiA+IDUwKSkgKyBnZW9tX2hpc3RvZ3JhbShiaW53aWR0aCA9IDEpICsgeGxhYigiSW5kaXZpZHVhbCBQZXJjZW50aWxlIFJhbmtpbmciKSArIHlsYWIoIk51bWJlciBvZiB1c2VycyIpCmBgYApgYGB7cn0KY2F0KCJOdW1iZXIgb2YgdXNlcnMgd2l0aCBiZXR0ZXIgdGhhbiByYW5kb20gcHJlZGljdGlvbnMgPSIsIGxlbmd0aChzdWJzZXQocmFua19maW5hbF9yZWNvbGFiLHJhbmtfZmluYWxfcmVjb2xhYjw1MCkpLCAiXG4iKQpjYXQoIk51bWJlciBvZiB1c2VycyB3aXRoIHdvcnNlIHRoYW4gcmFuZG9tIHByZWRpY3Rpb25zID0iLCBsZW5ndGgoc3Vic2V0KHJhbmtfZmluYWxfcmVjb2xhYixyYW5rX2ZpbmFsX3JlY29sYWI+PTUwKSksICJcbiIpCmBgYApMZXQncyBhbHNvIGxvb2sgYXQgdGhlIHNjYXR0ZXIgcGxvdCBiZXR3ZWVuIGluZGl2aWR1YWwgdXNlciBQUnMgYW5kIHRoZWlyIGZyZXF1ZW5jeSBvZiBwdXJjaGFzZQpgYGB7cn0KdXNlcl9jb3VudHNfdGVzdCA9IGFnZ3JlZ2F0ZShjYmluZCh0b3RhbCA9IGNvdW50KSB+IHVzZXJfaWQsIGRhdGE9dGVzdFssYygxOjMpXSwgRlVOID0gc3VtKQp1c2VyX2NvdW50c190ZXN0ID0gdXNlcl9jb3VudHNfdGVzdFtvcmRlcih1c2VyX2NvdW50c190ZXN0JHVzZXJfaWQpLF0KcHJlZF9BTFMgPSBnZXREYXRhLmZyYW1lKHJlY28sIGRlY29kZSA9IFRSVUUsIHJhdGluZ3MgPSBUUlVFKQp1c2Vyc19BTFMgPSBkYXRhLmZyYW1lKHVuaXF1ZShwcmVkX0FMUyR1c2VyKSxyYW5rX2ZpbmFsX3JlY29sYWIpCnVzZXJfY291bnRzX3Rlc3QkcmFua3NfQUxTID0gdXNlcnNfQUxTJHJhbmtfZmluYWxfcmVjb2xhYgpnZ3Bsb3QodXNlcl9jb3VudHNfdGVzdCkgKyBnZW9tX3BvaW50KGFlcyh4PXRvdGFsLCB5PXJhbmtzX0FMUywgY29sb3IgPSByYW5rc19BTFMgPiA1MCkpICsgeGxhYigiVG90YWwgcHVyY2hhc2VzIikgKyB5bGFiKCJJbmRpdmlkdWFsIFBlcmNlbnRpbGUgUmFua2luZyIpCmBgYApMb29rcyBsaWtlIHRoZSBsZWFybmluZyBpcyBxdWl0ZSB1bmlmb3JtIGFjcm9zcyBhbGwgdHlwZXMgb2YgdXNlcnMgKGZyZXF1ZW50IHZzIGluZnJlcXVlbnQgYnV5ZXJzKS4gTGV0J3MgYWxzbyBsb29rIGF0IHRoZSBwZXJjZW50YWdlcyBvZiB1c2VycyB3aG8gYm91Z2h0IDwgMjUgcHJvZHVjdHMKYGBge3J9CmNhdCgnJSBvZiB1c2VycyB3aG8gYm91Z2h0IDwgMjUgcHJvZHVjdHMgKG92ZXJhbGwpID0nLCBucm93KHN1YnNldCh1c2VyX2NvdW50c190ZXN0LHRvdGFsPDI1KSkvbnJvdyh1c2VyX2NvdW50c190ZXN0KSoxMDAsICIlXG4iKQpjYXQoJyUgb2YgdXNlcnMgd2hvIGJvdWdodCA8IDI1IHByb2R1Y3RzICh3aXRoIFBSID4gNTAlKSA9JywgbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCx0b3RhbDwyNSAmIHJhbmtzX0FMUyA+IDUwKSkvbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCwgcmFua3NfQUxTID4gNTApKSoxMDAsICIlXG4iKQpgYGAKVGhpcyB2ZXJpZmllcyB0aGUgZmFjdCB0aGF0IHRoZSBsZWFybmluZyBpcyBxdWl0ZSB1bmlmb3JtIGFjcm9zcyBkaWZmZXJlbnQgdHlwZXMgb2YgdXNlcnMKCjxicj4KCiMjIyMgRXhwbG9yaW5nIG1hdHJpeCBmYWN0b3JpemF0aW9uICh2aWEgInJlY29zeXN0ZW0iIHBhY2thZ2UpCgpMZXQncyBleHBsb3JlIHRoZSAicmVjb3N5c3RlbSIgbGlicmFyeS4gVGhpcyBwYWNrYWdlIGhhcyBhIG11Y2ggbW9yZSBlZmZpY2llbnQgaW1wbGVtZW50YXRpb24gdXNpbmcgU0dEIGFuZCBhbHNvIHN1cHBvcnRzIHBhcmFsbGVsL2NsdXN0ZXIgY29tcHV0YXRpb247IGFsdGhvdWdoIGZvciB0aGUgbW9yZSB0cmFkaXRpb25hbCBtYXRyaXggZmFjdG9yaXphdGlvbiwgd2hpY2ggKipkb2VzIG5vdCoqIG1vZGlmeSB0aGUgY29zdCBmdW5jdGlvbiBmb3IgaW1wbGljaXQgZmVlZGJhY2sgZGF0YQoKTm9uZXRoZWxlc3MsIEkgdGhvdWdodCBpdCB3b3VsZCBiZSBpbnRlcmVzdGluZyB0byBjb21wYXJlIGl0IHdpdGggdGhlIEFMUyBtZXRob2QuIEluIGFkZGl0aW9uIHRvIHR1bmluZyB0aGUgbnVtYmVyIG9mIGZhY3RvcnMsIGl0IHByb3ZpZGVzIGhvb2tzIHRvIHR1bmUgdGhlIGxlYXJuaW5nIHJhdGUgKHN0ZXAgc2l6ZSBpbiBTR0QpIGFuZCB0d28gc2VwYXJhdGUgcmVndWxhcml6YXRpb24gY29lZmZpY2llbnRzIGZvciB1c2VyIGFuZCBwcm9kdWN0IGZhY3RvcnMgZm9yIGJvdGggbDEgYW5kIGwyIG9wdGltaXplcnMuIAoKKipOb3RlOioqIExpa2UgZWFybGllciwgaWRlYWxseSwgd2Ugc2hvdWxkIG9wdGltaXplIGZvciBNUFIgd2hpbGUgZG9pbmcgcGFyYW1ldGVyIHR1bmluZy4gQnV0LCB3ZSdsbCBpZ25vcmUgdGhhdCBmb3Igbm93IGFuZCBvcHRpbWl6ZSB0aGUgZGVmYXVsdCBMMi1ub3JtCgpgYGB7cn0KcjIgPSBSZWNvKCkKCm9wdHMgPSByMiR0dW5lKGRhdGFfbWVtb3J5KHRyYWluJHVzZXJfaWQsdHJhaW4kcHJvZHVjdF9pZCx0cmFpbiRjb3VudCksIG9wdHMgPSBsaXN0KGRpbSA9IGMoMTAsMTAwLDIwMCksIGxyYXRlID0gYygwLjEsIDEsIDEwKSwgY29zdHBfbDIgPSBjKDAuMSwgMSwgMTApLCBjb3N0cV9sMiA9IGMoMC4xLCAxLCAxMCksIG50aHJlYWQgPSA0LCBuaXRlciA9IDIwKSkKYGBgCgpgYGB7cn0Kb3B0cyRtaW4KYGBgCgpOb3cgbGV0J3MgbGVhcm4gdGhlIG1vZGVsIG9uIGZ1bGwgdHJhaW4gZGF0YSB1c2luZyB0aGUgYmVzdCBwYXJhbWV0ZXJzIGNvbXB1dGVkIGFib3ZlCgpgYGB7cn0KcjIkdHJhaW4oZGF0YV9tZW1vcnkodHJhaW4kdXNlcl9pZCx0cmFpbiRwcm9kdWN0X2lkLHRyYWluJGNvdW50KSwgb3B0cyA9IGMob3B0cyRtaW4sIG50aHJlYWQgPSA0LCBuaXRlciA9IDEwMCkpCmBgYApOb3cgbGV0J3MgcGFkIHRoZSB0ZXN0IGRhdGEgd2l0aCAxMDAgcmFuZG9tIHNhbXBsZXMgZm9yIGVhY2ggdXNlciBhbmQgZXZhbHVhdGUgdGhlIG1vZGVsIGFuZCBwcmVkaWN0IHRoZSByYXRpbmdzLiBTaW5jZSB3ZSBkb3duc2FtcGxlZCB0aGUgZGF0YSwgcGFkZGluZyB3aXRoIDEwMCByYW5kb20gc2FtcGxlcyBpbnN0ZWFkIG9mIDEwMDAgc2hvdWxkIGJlIHN1ZmZpY2llbnQuCmBgYHtyfQp0ZXN0X3BhZGRlZCA9IHBhZFRlc3REYXRhKHRlc3QsdHJhaW4sMTAwMCkKcmVjbzIgPSByMiRwcmVkaWN0KGRhdGFfbWVtb3J5KHRlc3RfcGFkZGVkJHVzZXJfaWQsIHRlc3RfcGFkZGVkJHByb2R1Y3RfaWQpLCBvdXRfbWVtb3J5KCkpCnJhbmtzX01GID0gZXZhbHVhdGVNb2RlbDIodGVzdF9wYWRkZWQscmVjbzIpCmBgYAoKYGBge3J9CmNhdCgiTWVhbiBNUFIgYWNyb3NzIGFsbCB1c2VycyAoTUYpID0gIiwgbWVhbihyYW5rc19NRiksIlxuIikKY2F0KCJNZWRpYW4gTVBSIGFjcm9zcyBhbGwgdXNlcnMgKE1GKSA9ICIsIG1lZGlhbihyYW5rc19NRiksIlxuIikKYGBgCgpMb29rcyBsaWtlIHRoZSBtZWFuIGFuZCBtZWRpYW4gYXJlIG5vdCB0b28gZmFyIGFwYXJ0LiBMZXQncyBsb29rIGF0IHRoZSBoaXN0b2dyYW0gb2YgYWxsIHVzZXJzJyBQUgpgYGB7cn0KZ2dwbG90KGFzLmRhdGEuZnJhbWUocmFua3NfTUYpLCBhZXMocmFua3NfTUYsIGZpbGw9IHJhbmtzX01GID4gNTApKSArIGdlb21faGlzdG9ncmFtKGJpbndpZHRoID0gMSkgKyB4bGFiKCJJbmRpdmlkdWFsIFBlcmNlbnRpbGUgUmFua2luZyIpICsgeWxhYigiTnVtYmVyIG9mIHVzZXJzIikKYGBgCgoKYGBge3J9CmNhdCgiTnVtYmVyIG9mIHVzZXJzIHdpdGggYmV0dGVyIHRoYW4gcmFuZG9tIHByZWRpY3Rpb25zID0iLCBsZW5ndGgoc3Vic2V0KHJhbmtzX01GLHJhbmtzX01GPDUwKSksICJcbiIpCmNhdCgiTnVtYmVyIG9mIHVzZXJzIHdpdGggd29yc2UgdGhhbiByYW5kb20gcHJlZGljdGlvbnMgPSIsIGxlbmd0aChzdWJzZXQocmFua3NfTUYscmFua3NfTUY+PTUwKSksICJcbiIpCmBgYApMZXQncyBhbHNvIGxvb2sgYXQgdGhlIHNjYXR0ZXIgcGxvdCBiZXR3ZWVuIGluZGl2aWR1YWwgdXNlciBQUnMgYW5kIHRoZWlyIGZyZXF1ZW5jeSBvZiBwdXJjaGFzZQpgYGB7cn0KcHJlZF9NRiA9IGRhdGEuZnJhbWUodGVzdF9wYWRkZWRbLDFdLHRlc3RfcGFkZGVkWywyXSxyZWNvMikKdXNlcnNfTUYgPSBkYXRhLmZyYW1lKHVuaXF1ZShwcmVkX01GJHVzZXJfaWQpLHJhbmtzX01GKQp1c2VyX2NvdW50c190ZXN0JHJhbmtzX01GID0gdXNlcnNfTUYkcmFua3NfTUYKZ2dwbG90KHVzZXJfY291bnRzX3Rlc3QpICsgZ2VvbV9wb2ludChhZXMoeD10b3RhbCwgeT1yYW5rc19NRiwgY29sb3IgPSByYW5rc19NRiA+IDUwKSkgKyB4bGFiKCJUb3RhbCBwdXJjaGFzZXMiKSArIHlsYWIoIkluZGl2aWR1YWwgUGVyY2VudGlsZSBSYW5raW5nIikKYGBgCioqT2JzZXJ2YXRpb246KiogSXQgaXMgdmVyeSBldmlkZW50IHRoYXQgdGhlIHBlcmZvcm1hbmNlIG9mIE1GIGlzIHBvb3IgbWFpbmx5IGZvciB1c2VycyB3aG8gYXJlIGluZnJlcXVlbnQgYnV5ZXJzLCB3aGljaCB3YXMgbm90IHRoZSBjYXNlIGZvciBBTFMuIFRoaXMgaXMgZXhwZWN0ZWQgYmVjYXVzZSBBTFMgaXMgdHVuZWQgZm9yIGltcGxpY2l0IGZlZWRiYWNrIGRhdGEgd2hlcmUgYW4gaW5mcmVxdWVudCB1c2VyIGRvZXNuJ3QgY29ycmVzcG9uZCB0byBhIHVzZXIgd2hvICoiZGlzbGlrZXMiKiB0aG9zZSBwcm9kdWN0LiBXaGlsZSB0aGUgTUYgd291bGQgdGhpbmsgdGhhdCBpbmZyZXF1ZW50IHVzZXJzICJkaXNsaWtlIiB0aGF0IHByb2R1Y3QgYW5kIHdvdWxkIHRoZXJlZm9yZSBtYWtlIHdyb25nIHByZWRpY3Rpb25zIGFib3V0IHdoYXQgdGhleSBtaWdodCBidXkgaW4gdGhlIGZ1dHVyZS4gKipJbiBwYXJ0aWN1bGFyLCB0aGlzIHdvdWxkIG1lYW4gdGhhdCB0aGUgTUYgYWxnb3JpdGhtIHdvdWxkIE5PVCByZWNvbW1lbmQgcHJvZHVjdHMgd2hpY2ggYXJlIHNpbWlsYXIgdG8gdGhlIG9uZXMgdGhlIGluZnJlcXVlbnQgdXNlcnMgYWxyZWFkeSBib3VnaHQqKiAtLSB3aGljaCBpcyBub3QgY29ycmVjdAoKQWxtb3N0IGFsbCBvZiB0aGUgY2FzZXMgd2hlcmUgdGhlIHBlcmZvcm1hbmNlIGlzIHdvcnNlIHRoYW4gNTAlIGFyZSBmb3IgdXNlcnMgd2hvIGhhdmUgYm91Z2h0IGxlc3MgdGhhbiAyNSBwcm9kdWN0cyAodXBwZXIgbGVmdCBjb3JuZXIpLiBMaWtlIGRpc2N1c3NlZCBlYXJsaWVyLCBhIGJldHRlciBkb3duLXNhbXBsaW5nIHNjaGVtZSB3aGljaCByZXN1bHRzIGluIGEgbW9yZSBkZW5zZSBtYXRyaXggbWlnaHQgeWllbGQgYmV0dGVyIHBlcmZvcm1hbmNlcyB3aGVyZSB3ZSBjYW4gbGV2ZXJhZ2UgbW9yZSBmcm9tIGZyZXF1ZW50IChwb3dlcikgdXNlcnMKCmBgYHtyfQpjYXQoJyUgb2YgdXNlcnMgd2hvIGJvdWdodCA8IDI1IHByb2R1Y3RzIChvdmVyYWxsKSA9JywgbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCx0b3RhbDwyNSkpL25yb3codXNlcl9jb3VudHNfdGVzdCkqMTAwLCAiJVxuIikKY2F0KCclIG9mIHVzZXJzIHdobyBib3VnaHQgPCAyNSBwcm9kdWN0cyAod2l0aCBQUiA+IDUwJSkgPScsIG5yb3coc3Vic2V0KHVzZXJfY291bnRzX3Rlc3QsdG90YWw8MjUgJiByYW5rc19NRiA+IDUwKSkvbnJvdyhzdWJzZXQodXNlcl9jb3VudHNfdGVzdCwgcmFua3NfTUYgPiA1MCkpKjEwMCwgIiVcbiIpCmBgYAoKCiMjIyBTdW1tYXJ5CgojIyMjIERhdGE6CisgTnVtYmVyIG9mIHVzZXJzOiAxMSwyNjMKKyBOdW1iZXIgb2YgcHJvZHVjdHM6IDMsMTM4CisgVHJhaW4gZGF0YSBwb2ludHM6IDc5LDMwNyAoNjglKSAKKyBUZXN0IGRhdGEgcG9pbnRzOiAzNywzMjggKDMyJSkKCiMjIyMgT3V0cHV0OgorIE1lYW4gUGVyY2VudGlsZSBSYW5rIHVzaW5nIFdlaWdodGVkIEFsdGVybmF0aW5nIExlYXN0IFNxdWFyZXMgbWV0aG9kIChIdSBldC4gYWwuKTogKioxMy40OSUqKiAoTWVkaWFuOiA3LjQxJSkKKyBNZWFuIFBlcmNlbnRpbGUgUmFuayB1c2luZyBNYXRyaXggRmFjdG9yaXphdGlvbjogKioyNy44MCUqKiAoTWVkaWFuOiAyNC43MSUpCgoKIyMjIERpc2N1c3Npb24KVGhlIGV4cGVyaW1lbnRzIHBlcmZvcm1lZCBoZXJlIGFyZSBleHBsb3JhdG9yeSBpbiBuYXR1cmUgYW5kIGRvIG5vdCByZWZsZWN0IHRoZSB0cnVlIHBvdGVudGlhbCBvZiB0aGVzZSBhbGdvcml0aG1zLiBUaGVyZSBpcyBhIGxvdCBvZiByb29tIGZvciBpbXByb3ZlbWVudDoKCjEuICoqRGF0YSBzZXQ6KiogV2UgdXNlZCBvbmx5IGEgc21hbGwgc3Vic2V0IG9mIHRoZSBkYXRhIChhYm91dCA1LjUlIG9mIHVzZXJzIGFuZCA2LjUlIG9mIHByb2R1Y3RzKS4gVGhlIGdvYWwgb2YgbXkgZXhwZXJpbWVudHMgd2VyZSB0byBtYWtlIHRoZW0gcnVubmFibGUgb24gYSBzaW5nbGUgY29tcHV0ZXIuIEJ5IGRlcGxveWluZyB0aGVzZSBhbGdvcml0aG1zIG9uIGEgc3BhcmsgY2x1c3RlciBvdmVyIHRoZSBlbnRpcmUgZGF0YSBjYW4gZ2l2ZSBtdWNoIGJldHRlciByZXN1bHRzLgoyLiAqKlNhbXBsaW5nOioqIExpa2UgZGVzY3JpYmVkIGFib3ZlLCB3ZSBjYW4gdXNlIGEgaHlicmlkIHNhbXBsaW5nIGFwcHJvYWNoIHNvIHRoYXQgZXZlbiB3aXRoIHRoZSBzbWFsbCBzdWJzZXQgb2YgZGF0YSwgd2UgY2FuIGdldCBhIGJldHRlciByZXByZXNlbnRhdGlvbiBvZiBkaWZmZXJlbnQgdHlwZXMgb2YgdXNlcnMgYW5kIHByb2R1Y3RzIGJhc2VkIG9uIHRoZWlyIGZyZXF1ZW5jeS4gVGhpcyBtaWdodCBoZWxwIGltcHJvdmUgdGhlIHJlc3VsdHMgZnVydGhlci4KMy4gKipQYXJhbWV0ZXIgdHVuaW5nOioqIEJldHRlci9tb3JlIHBhcmFtZXRlciB0dW5pbmcgY291bGQgaGF2ZSByZXN1bHRlZCBpbiBiZXR0ZXIgTVBSczoKICAgICsgVGhlIG51bWJlciBvZiBjb21iaW5hdGlvbnMgZ3JvdyBleHBvbmVudGlhbGx5IHdpdGggZWFjaCBuZXcgdmFsdWUgb2YgYSBoeXBlcnBhcmFtZXRlciB3aGljaCBhZmZlY3RzIHRoZSBjb21wdXRhdGlvbiB0aW1lIGZvciBjcm9zcy12YWxpZGF0aW9uLiBXZSBjb3VsZCBkbyBtb3JlIG9wdGltaXplZCBwYXJhbWV0ZXIgdHVuaW5nIHVzaW5nIGNvYXJzZSB0byBmaW5lIG1ldGhvZHMuCiAgICArIEZvciBBbHRlcm5hdGluZyBMZWFzdCBTcXVhcmVzIG1ldGhvZCwgd2UgZGlkbid0IGRvIHRyYWRpdGlvbmFsIGNyb3NzLXZhbGlkYXRpb24sIGJ1dCBhIHZhcmlhbnQgb2YgaXQgdXNpbmcgc21hbGxlciBkYXRhIHNldHMuIFRyYWRpdGlvbmFsIGNyb3NzLXZhbGlkYXRpb24gY291bGQgaGF2ZSB5aWVsZWQgYmV0dGVyIGh5cGVycGFyYW1ldGVyIGVzdGltYXRlcyBiZWNhdXNlIGl0IHdvcmtzIG9uIGxhcmdlciBzbGljZXMgb2YgdHJhaW4vdmFsaWRhdGlvbiBkYXRhLgogICAgKyBGb3IgTWF0cml4IEZhY3Rvcml6YXRpb24gbWV0aG9kLCB3ZSB1c2VkIHRoZSBidWlsdC1pbiBwYXJhbWV0ZXIgdHVuaW5nIHdoaWNoIGRvZXNuJ3Qgb3B0aW1pemUgdGhlIE1QUi4gV3JpdGluZyBvdXIgb3duIG1ldGhvZHMgZm9yIGRvaW5nIHRoYXQgbWlnaHQgaGF2ZSBnaXZlbiBiZXR0ZXIgaHlwZXJwYXJhbWV0ZXIgZXN0aW1hdGVzLiBXZSBjb3VsZCBhbHNvIHRyeSBtaW5pbWl6aW5nIHRoZSBMMSBsb3NzIGluc3RlYWQgb2YgTDIgZm9yIGJvdGggdGhlIGxhbWJkYSB2YWx1ZXMKNC4gKipBbGdvcml0aG1zOioqIFdlIGNvdWxkIGFsc28gbG9vayBhdCB0aGUgZGlmZmVyZW50IGZvcm11bGF0aW9ucyBvZiB0aGUgYWxnb3JpdGhtcy4gQW4gaW1wb3J0YW50IHZhcmlhdGlvbiBpbnZvbHZlcyBpbmNsdWRpbmcgdGhlIGJpYXMgdGVybXMgZm9yIGV2ZXJ5IHVzZXIgYW5kIHByb2R1Y3QuIFRoaXMgd291bGQgZW5jb2RlIGNlcnRhaW4gdXNlcnMnIHByZWZlcmVuY2UgdG8gYnV5IG1vcmUvZGl2ZXJzZSBzZXQgb2YgcHJvZHVjdHMgdnMgb3RoZXJzIGFuZCBjZXJ0YWluIHByb2R1Y3RzIGJlaW5nIGdlbmVyYWxseSBtb3JlIHBvcHVsYXIvaW1wb3J0YW50IHRoYW4gb3RoZXJzLiBUaGUgQUxTIGFsZ29yaXRobSBjYW4gYmUgZWFzaWx5IG1vZGlmaWVkIHRvIGludHJvZHVjZSB0aGVzZSBiaWFzZXMgKFtsaWtlIHN1Z2dlc3RlZCBoZXJlXShodHRwOi8vYWN0aXZpc2lvbmdhbWVzY2llbmNlLmdpdGh1Yi5pby8yMDE2LzAxLzExL0ltcGxpY2l0LVJlY29tbWVuZGVyLVN5c3RlbXMtQmlhc2VkLU1hdHJpeC1GYWN0b3JpemF0aW9uLykpLiBMaWtld2lzZSwgSm9obnNvbidzIGFwcHJvYWNoIHVzaW5nIFtMb2dpc3RpYyBNYXRyaXggRmFjdG9yaXphdGlvbl0oaHR0cHM6Ly9zdGFuZm9yZC5lZHUvfnJlemFiL25pcHMyMDE0d29ya3Nob3Avc3VibWl0cy9sb2dtYXQucGRmKSBjYW4gYWxzbyBiZSB1c2VkLiBUaGF0IHNhaWQsIEkgYmVsaWV2ZSB0aGUgYmlnZ2VzdCBnYWlucyB3aWxsIGxpa2VseSBjb21lIGZyb20gdXNpbmcgbW9yZSBkYXRhLg==