Introduction

The goal for the final project is to build out a recommender system using a large dataset (ex: 1M+ ratings or 10k+ users, 10k+ items. There are three deliverable, with separate dates:

The overall goal, however, will be to produce quality recommendations by extracting insights from a large dataset.

I intend to build recommendation system that will produce quality recommendation using the Recommenderlab we covered in class and experiment with Apriori Algorithm for Association Rules which is different from what we did in class.

The ultimate goal of using both approaches is to compare the items recommended using algorithm selected from the Recommenderlab and that of Apriori Association Rule.

Project Highlights

Recommenderlab accepts 2 types of rating matrix for modelling:

Real rating matrix consisting of actual user ratings, which requires normalisation.

Binary rating matrix consisting of 0’s and 1’s, where 1’s indicate if the product was purchased. This is the matrix type needed for the analysis and it does not require normalization.

  1. I used the recommenderlab, an R package which provides a convenient framework to evaluate and compare various recommendation algorithms and quickly establish the best suited approach.

  2. I arranged the purchase history in a rating matrix, with orders in rows and products in columns. This format is often called a user_item matrix because “users” (e.g. customers or orders) tend to be on the rows and “items” (e.g. products) on the columns.

  3. The recommenderlab has an ability to estimate multiple algorithms at a time, I will create a list with the algorithms and consider schemes which evaluate on a binary rating matrix.

  4. I compared and evaluated the performance of the algorithms using ROC Curve, Precision/Recall, RMSE, MSE and MAE and use the best performing algorithm to predict top items list for the new customer.

  5. Finally, I deployed the algorithm using Shiny App from R

About the Data

The data for this project comes from the UCI Machine Learning Repository, an online archive of large datasets which includes a wide variety of data types, analysis tasks, and application areas.

In this project I used the Online Retail dataset donated to UCI in 2015 by the School of Engineering at London South Bank University.

This dataset is common and have been used in many Market Basket Analysis; the dataset contains transactions occurring between 01/Dec/2010 and 09/Dec/2011 for a UK-based and registered online retail company and contains 541,909 observations with eight variables.

The data is too large for my GitHub but can be downloaded from: http://archive.ics.uci.edu/ml/machine-learning-databases/00352/

library(ggplot2)
library(plyr)
library(kableExtra)
library(recommenderlab)
library(data.table)           
library(readxl)               
library(tidyverse)
library(skimr)                
library(knitr)                
library(treemap)
library(RColorBrewer)
library(arules)
library(arulesViz)

Loading the Online Retail dataset


retail <- read_excel("C:/Users/Emahayz_Pro/Desktop/Data_Science/Data 612/Week6-Final Project Proposal/Online Retail.xlsx",trim_ws = TRUE)

Data structure

str(retail)
tibble [541,909 x 8] (S3: tbl_df/tbl/data.frame)
 $ InvoiceNo  : chr [1:541909] "536365" "536365" "536365" "536365" ...
 $ StockCode  : chr [1:541909] "85123A" "71053" "84406B" "84029G" ...
 $ Description: chr [1:541909] "WHITE HANGING HEART T-LIGHT HOLDER" "WHITE METAL LANTERN" "CREAM CUPID HEARTS COAT HANGER" "KNITTED UNION FLAG HOT WATER BOTTLE" ...
 $ Quantity   : num [1:541909] 6 6 8 6 6 2 6 6 6 32 ...
 $ InvoiceDate: POSIXct[1:541909], format: "2010-12-01 08:26:00" "2010-12-01 08:26:00" "2010-12-01 08:26:00" ...
 $ UnitPrice  : num [1:541909] 2.55 3.39 2.75 3.39 3.39 7.65 4.25 1.85 1.85 1.69 ...
 $ CustomerID : num [1:541909] 17850 17850 17850 17850 17850 ...
 $ Country    : chr [1:541909] "United Kingdom" "United Kingdom" "United Kingdom" "United Kingdom" ...

Remove the Cancelled Transaction

retail  <- retail %>% 
  filter(!grepl("C", retail$InvoiceNo))

Excluding the missing values

retail <- retail %>% 
  filter(!is.na(Description))

retail <- retail %>% 
  filter(!is.na(CustomerID))

Remove duplicate items

Creating and dropping the unique identifier to filter the duplicates

retail <- retail %>% 

    mutate(InNo_Desc = paste(InvoiceNo, Description, sep = ' ')) 

    retail <- retail[!duplicated(retail$InNo_Desc), ] %>% 
    select(-InNo_Desc)

Data summary

summary(retail)
  InvoiceNo          StockCode         Description           Quantity       
 Length:387695      Length:387695      Length:387695      Min.   :    1.00  
 Class :character   Class :character   Class :character   1st Qu.:    2.00  
 Mode  :character   Mode  :character   Mode  :character   Median :    6.00  
                                                          Mean   :   13.26  
                                                          3rd Qu.:   12.00  
                                                          Max.   :80995.00  
  InvoiceDate                    UnitPrice          CustomerID      Country         
 Min.   :2010-12-01 08:26:00   Min.   :   0.000   Min.   :12346   Length:387695     
 1st Qu.:2011-04-07 10:24:00   1st Qu.:   1.250   1st Qu.:13941   Class :character  
 Median :2011-07-29 14:29:00   Median :   1.950   Median :15144   Mode  :character  
 Mean   :2011-07-10 12:01:53   Mean   :   3.121   Mean   :15282                     
 3rd Qu.:2011-10-20 11:03:00   3rd Qu.:   3.750   3rd Qu.:16789                     
 Max.   :2011-12-09 12:50:00   Max.   :8142.750   Max.   :18287                     

Pre-processing

Convert data to numeric

sapply(retail[ ,c('InvoiceNo','CustomerID')], 
               function(x) length(unique(x)))
 InvoiceNo CustomerID 
     18536       4339 
retail <- retail %>%
  mutate(Description = as.factor(Description)) %>%
  mutate(Country = as.factor(Country)) %>% 
  mutate(InvoiceNo = as.numeric(InvoiceNo)) %>% 
  mutate(Date = as.Date(InvoiceDate)) %>% 
  mutate(Time = as.factor(format(InvoiceDate,"%H:%M:%S")))

Exploring the dataset

View the Distributions

treemap(retail,
        index      = c("Country"),
        vSize      = "Quantity",
        algorithm  = "pivotSize",
        title      = "The Country with the Most Purchase",
        palette    = "Set3",
        border.col = "grey20")

Create the Rating Matrix

ratingMat <- retail %>%
  select(InvoiceNo, Description) %>% 
  mutate(value = 1) %>% 
  spread(Description, value, fill = 0) %>%
  select(-InvoiceNo) %>% 
  as.matrix() %>% 
  as("binaryRatingMatrix")
ratingMat
18536 x 3866 rating matrix of class ‘binaryRatingMatrix’ with 387695 ratings.

Algorithms- Modeling

Set up List of Algorithms

Scheme <- ratingMat %>% 
  evaluationScheme(method = "cross",
                   k      = 5, 
                   train  = 0.8,  
                   given  = -1)
Scheme
Evaluation scheme using all-but-1 items
Method: ‘cross-validation’ with 5 run(s).
Good ratings: NA
Data set: 18536 x 3866 rating matrix of class ‘binaryRatingMatrix’ with 387695 ratings.
ModelAlgorithms <- list(
  "Association" = list(name  = "AR", 
                        param = list(supp = 0.01, conf = 0.8)),
  "UserBased" = list(name="UBCF", param=list(method="Cosine",
                                                 nn=500)),
  "ItemBased" = list(name="IBCF", param=list(k = 5))
  
  )

Estimate the Models

Using n=c(1, 5, 10, 15, 20, 25) to evaluate product recommendations

ModelResults <- evaluate(Scheme, ModelAlgorithms, n=c(1, 5, 10, 15, 20, 25))
AR run fold/sample [model time/prediction time]
     1  [0.26sec/43.32sec] 
     2  [0.21sec/40.63sec] 
     3  [0.22sec/40.91sec] 
     4  [0.18sec/40.78sec] 
     5  [0.2sec/42.16sec] 
UBCF run fold/sample [model time/prediction time]
     1  [0sec/393.43sec] 
     2  [0sec/382.14sec] 
     3  [0.02sec/376.9sec] 
     4  [0sec/406.61sec] 
     5  [0sec/402.96sec] 
IBCF run fold/sample [model time/prediction time]
     1  [240.3sec/1.99sec] 
     2  [239.71sec/2.29sec] 
     3  [247.3sec/2.64sec] 
     4  [242.91sec/2.69sec] 
     5  [242.28sec/2.44sec] 

It seems the IBCF took longer to estimate

Comprison of the Model Accuracy

ROC curve

plot(ModelResults, annotate = TRUE, legend = "topleft")
title("ROC Curve")

Precision and Recall

plot(ModelResults, "prec/rec", annotate = TRUE) # precision and recall
title("Precision and Recall")

Classification models performance can be compared using the ROC curve, which plots the true positive rate (TPR) against the false positive rate (FPR).

The item-based collaborative filtering model is the winner as it achieves the highest TPR for any given level of FPR.

This means that the model is producing the highest number of relevant recommendations known as True Positives(TP)) for the same level of non-relevant recommendations known as False Positives(FP)).

Training, Testing and Evaluation Data sets

Prepare training dataset

train <- getData(Scheme, "train")

Prepare testing set

test <- getData(Scheme, "known")

Prepare evaluation set

evaluation <- getData(Scheme, "unknown")

Recommender System

Best Performing Model - Item Based

Itemmodel <- Recommender(train, method = "IBCF",  
                       param = list(k = 5))

Making Predictions with newdata = test

Itempred <- predict(Itemmodel, newdata = test)

Model Evaluation

Itemaccuracy <- calcPredictionAccuracy(Itempred, evaluation, given=10)
kable(Itemaccuracy,caption = "Performance Metrics") 
Performance Metrics
x
TP 0.2880259
FP 8.6933657
FN 0.7119741
TN 3846.3066343
precision 0.0343062
recall 0.2880259
TPR 0.2880259
FPR 0.0022551

Using Association Rules

Transaction Data

Create and view the transaction data since Apriori algorithm only works with transaction object

retailtransaction <- ddply(retail,c("InvoiceNo","Date"),
                       function(df1)paste(df1$Description,
                       collapse = ","))
retailtransaction

Transaction Basket for Items

Cast Invoice and Date to NULL since they’re not required and create items Basket

retailtransaction$InvoiceNo <- NULL
retailtransaction$Date <- NULL
colnames(retailtransaction) <- c("items")
retailtransaction

Transaction Object

Save the transaction data as csv and upload it to create transaction Object.

write.csv(retailtransaction,"C:/Users/Emahayz_Pro/Desktop/Data_Science/Data 612/Week7-Final Project Submission/retailtransactions.csv", quote = FALSE, row.names = FALSE)

retailtrans <- read.transactions("C:/Users/Emahayz_Pro/Desktop/Data_Science/Data 612/Week7-Final Project Submission/retailtransactions.csv", format = 'basket', rm.duplicates = FALSE, sep=',')
retailtrans
transactions in sparse format with
 18537 transactions (rows) and
 7725 items (columns)

Transaction Summary

summary (retailtrans)
transactions as itemMatrix in sparse format with
 18537 rows (elements/itemsets/transactions) and
 7725 columns (items) and a density of 0.002292625 

most frequent items:
WHITE HANGING HEART T-LIGHT HOLDER           REGENCY CAKESTAND 3 TIER 
                              1759                               1533 
           JUMBO BAG RED RETROSPOT                      PARTY BUNTING 
                              1417                               1266 
     ASSORTED COLOUR BIRD ORNAMENT                            (Other) 
                              1244                             321081 

element (itemset/transaction) length distribution:
sizes
   1    2    3    4    5    6    7    8    9   10   11   12   13   14   15   16   17   18   19   20 
1574  865  757  758  759  708  653  644  637  584  611  526  510  518  545  515  460  436  483  415 
  21   22   23   24   25   26   27   28   29   30   31   32   33   34   35   36   37   38   39   40 
 400  306  304  275  235  251  233  208  219  209  165  157  133  142  133  109  112   86  109   95 
  41   42   43   44   45   46   47   48   49   50   51   52   53   54   55   56   57   58   59   60 
  94   83   91   67   62   70   64   57   58   51   64   41   42   48   44   37   31   39   31   28 
  61   62   63   64   65   66   67   68   69   70   71   72   73   74   75   76   77   78   79   80 
  25   19   25   25   22   27   25   24   14   19   17   12   12   15   10   16   13    7    7   15 
  81   82   83   84   85   86   87   88   89   90   91   92   93   94   95   96   97   98   99  100 
  15   12    7    9   10   11   12    8    6    5    6   11    6    3    4    3    6    4    1    4 
 101  102  103  104  105  106  107  108  109  110  111  113  114  116  117  118  120  122  123  125 
   3    4    4    3    2    2    5    4    4    2    5    3    3    3    3    4    2    2    1    2 
 126  127  131  132  133  134  136  140  141  142  143  145  146  147  150  151  154  157  168  171 
   2    2    1    1    2    1    1    1    2    2    1    2    1    1    1    1    2    2    2    2 
 177  178  180  202  204  228  236  249  250  285  320  400  419 
   1    1    1    1    1    1    1    1    1    1    1    1    1 

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   1.00    5.00   13.00   17.71   23.00  419.00 

includes extended item information - examples:

There are 18537 transactions (rows) and 7725 items (columns). Note that 7725 is the product descriptions involved in the dataset and 18537 transactions are collections of these items.

Density tells the percentage of non-zero cells in a sparse matrix. We can say it as the total number of items that are purchased divided by a possible number of items in that matrix.

Item Frequency Plot

I can generate an itemFrequencyPlot to create an item Frequency Bar Plot to view the distribution of objects based on itemMatrix using absolute item frequency. Here, I will view the top 20 items.

itemFrequencyPlot(retailtrans,topN=20,type="absolute",col=brewer.pal(8,'Pastel2'), main="Absolute Item Frequency Plot")

Constructing the model- Association Rules

Next step is to mine the rules using the APRIORI algorithm. The function apriori() is from package arules.

Using Min Support as 0.001, confidence as 0.8.

Association Rules

Control The Number Of Rules

I can adjust the maxlen, supp and conf arguments in the apriori function to control the number of rules generated.

• To get ‘strong‘ rules, I can increase the value of ‘conf’ parameter, here we are using 0.8 which is high
at 80%. • To get ‘longer‘ rules, I can increase ‘maxlen’, here we are using the minimum number of length (2) that can generate a rule which is the first attempt for this transaction data.

retailrules <- apriori(retailtrans, parameter = list(supp = 0.001, conf = 0.8, maxlen = 2))
Apriori

Parameter specification:

Algorithmic control:

Absolute minimum support count: 18 

set item appearances ...[0 item(s)] done [0.00s].
set transactions ...[7725 item(s), 18537 transaction(s)] done [0.22s].
sorting and recoding items ... [2442 item(s)] done [0.01s].
creating transaction tree ... done [0.01s].
checking subsets of size 1 2
Mining stopped (maxlen reached). Only patterns up to a length of 2 returned!
 done [0.11s].
writing ... [113 rule(s)] done [0.01s].
creating S4 object  ... done [0.01s].
summary(retailrules)
set of 113 rules

rule length distribution (lhs + rhs):sizes
  2 
113 

   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
      2       2       2       2       2       2 

summary of quality measures:
    support           confidence        coverage             lift            count       
 Min.   :0.001079   Min.   :0.8000   Min.   :0.001187   Min.   : 24.33   Min.   : 20.00  
 1st Qu.:0.002643   1st Qu.:0.8448   1st Qu.:0.002751   1st Qu.: 97.73   1st Qu.: 49.00  
 Median :0.004100   Median :0.9167   Median :0.004262   Median :105.16   Median : 76.00  
 Mean   :0.005169   Mean   :0.9187   Mean   :0.005682   Mean   :193.34   Mean   : 95.81  
 3rd Qu.:0.007984   3rd Qu.:1.0000   3rd Qu.:0.008793   3rd Qu.:337.04   3rd Qu.:148.00  
 Max.   :0.022280   Max.   :1.0000   Max.   :0.027243   Max.   :597.97   Max.   :413.00  

mining info:

Apriori created 113 rules for maxlen = 2, I decided to use maxlen = 2 because greater than 2 will create rules in the thousands which I want to avoid.

The maximum support is 0.022280, the average confidence is 0.9187 and maximum Confidence is 1. The average lift is 193.34 and the maximum lift is 597.97

Let’s view the Top 10 rules

inspect(retailrules[1:10])
     lhs                                    rhs                                      support confidence    coverage      lift count
[1]  {DOG LICENCE WALL ART}              => {BICYCLE SAFTEY WALL ART}            0.001078923  0.9090909 0.001186816 510.66116    20
[2]  {WOBBLY CHICKEN}                    => {METAL}                              0.001456546  1.0000000 0.001456546 378.30612    27
[3]  {WOBBLY CHICKEN}                    => {DECORATION}                         0.001456546  1.0000000 0.001456546 378.30612    27
[4]  {MIXED NUTS LIGHT GREEN BOWL}       => {SMALL DOLLY MIX DESIGN ORANGE BOWL} 0.001078923  0.9090909 0.001186816  60.18506    20
[5]  {MIRRORED WALL ART LADIES}          => {MIRRORED WALL ART GENTS}            0.001132869  0.8400000 0.001348654 556.11000    21
[6]  {DECOUPAGE}                         => {GREETING CARD}                      0.001240762  1.0000000 0.001240762 331.01786    23
[7]  {BILLBOARD FONTS DESIGN}            => {WRAP}                               0.001564439  1.0000000 0.001564439 597.96774    29
[8]  {WRAP}                              => {BILLBOARD FONTS DESIGN}             0.001564439  0.9354839 0.001672331 597.96774    29
[9]  {SET/4 BLUE FLOWER CANDLES IN BOWL} => {S/4 PINK FLOWER CANDLES IN BOWL}    0.001078923  0.8000000 0.001348654 138.59439    20
[10] {ENAMEL PINK TEA CONTAINER}         => {ENAMEL PINK COFFEE CONTAINER}       0.001672331  0.8157895 0.002049954 321.75084    31

The Top 20 Lift Rules

I will also try to remove redundant rules

retailrules_Lift <- sort (retailrules, by = "lift", decreasing=TRUE)
Groceries.rules_Lift <- retailrules_Lift[!is.redundant(retailrules_Lift)]

# show the support, lift and confidence for the rules
inspect(head(retailrules_Lift, 20))

write(retailrules_Lift, 
      file = "Apriori Recommended Items.csv",
      sep = ",",
      quote = TRUE,
      row.names = FALSE
      )

The rules with confidence of 1 as seen above implies that, whenever the LHS item was purchased, the RHS item was also purchased 100% of the time.

A rule with a lift of 597 as seen above implies that, the items in LHS and RHS are 597 times more likely to be purchased together compared to the purchases when they are assumed to be unrelated.

Ploting the rules

Plotting all the 113 rules will be too much, so I will subset the rules with confidence of at least 90%.

retailsubrules<-retailrules_Lift[quality(retailrules_Lift)$confidence>0.9]
plot(retailsubrules)

The chart shows the scatter of the reduced number of rules (66).

Let’s select 20 rules having the highest confidence.

Top 20 Rules

topretailsubrules <- head(retailsubrules, n = 20, by = "confidence")

Graph of Top 20 Rules

plot(topretailsubrules, method = "graph", layout=igraph::in_circle())

Interactive Graph Using Plotly

Using the interactive plot, I can see the recommended items from the LHS and the matching items on the RHS with the respective support, lift and confidence.

plotly_arules(topretailsubrules)

I can also interactively select certain rules to view the recommended items

plot(topretailsubrules, method = "graph",  engine = "htmlwidget")

Conclusion

The purpose of this project was to build a recommender system and to practice working with apriori association rules to recommend items for possible purchases.

For the given data set, the Item-based Model within recommenderlab provided the best results. It is worth remembering that the choice of the recommender algorithm depends on the context. The accuracy of the Item-based model is acceptable compared to the other algorithms considered.

In addition to the methods provided within the recommenderlab library, there are other supervised and unsupervised machine learning algorithm as well as neutral network-driven deep learning algorithms that could be used to build recommendation systems.

However, I used the unsupervised Apriori Algorithm for Association rules and was able to obtain the recommended items. For each given rule, the items on the LHS have a matching items on the RHS indicating the confidence level to which these items were purchased together. The recommended items for top 20 rules with the corresponding support, lift and confidence are printed and saved as a csv file that is submitted with this project.

Finally, I created an interactive graph to see the recommended items from the LHS and the matching items on the RHS with the respective support, lift and confidence. The second interactive graph shows where I can select any rule and view the recommended items.

References

Breese JS, Heckerman D, Kadie C (1998). “Empirical Analysis of Predictive Algorithms for Collaborative Filtering.” In Uncertainty in Artificial Intelligence. Proceedings of the Fourteenth Conference, pp. 43-52.

Jabeen, H. (2018). Market Basket Analysis using R. Retrieved from https://www.datacamp.com/community/tutorials/market-basket-analysis-r

Kohavi, Ron (1995). “A study of cross-validation and bootstrap for accuracy estimation and model selection”. Proceedings of the Fourteenth International Joint Conference on Artificial Intelligence, pp. 1137-1143.

Koren, Y., Bell, R., & Volinsky, C. (2009). Matrix Factorization Techniques for Recommender Systems. Computer, 42(8), 30–37. https://doi.org/10.1109/MC.2009.263

Michael Hahsler (2016). recommenderlab: A Framework for Developing and Testing Recommendation Algorithms, R package. https://CRAN.R-project.org/package=recommenderlab

LS0tDQp0aXRsZTogIkRhdGEgNjEyIC0gRmluYWwgUHJvamVjdCINCmF1dGhvcjogIkVtbWFudWVsIEhheWJsZS1Hb21lcyINCmRhdGU6ICIwNy8xNC8yMDIwIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQogICAgDQotLS0NCg0KIyBJbnRyb2R1Y3Rpb24NCg0KVGhlIGdvYWwgZm9yIHRoZSBmaW5hbCBwcm9qZWN0IGlzIHRvIGJ1aWxkIG91dCBhIHJlY29tbWVuZGVyIHN5c3RlbSB1c2luZyBhIGxhcmdlIGRhdGFzZXQgKGV4OiAxTSsgcmF0aW5ncyBvciAxMGsrIHVzZXJzLCAxMGsrIGl0ZW1zLiBUaGVyZSBhcmUgdGhyZWUgZGVsaXZlcmFibGUsIHdpdGggc2VwYXJhdGUgZGF0ZXM6DQoNClRoZSBvdmVyYWxsIGdvYWwsIGhvd2V2ZXIsIHdpbGwgYmUgdG8gcHJvZHVjZSBxdWFsaXR5IHJlY29tbWVuZGF0aW9ucyBieSBleHRyYWN0aW5nIGluc2lnaHRzIGZyb20gYSBsYXJnZSBkYXRhc2V0LiANCg0KSSBpbnRlbmQgdG8gYnVpbGQgcmVjb21tZW5kYXRpb24gc3lzdGVtIHRoYXQgd2lsbCBwcm9kdWNlIHF1YWxpdHkgcmVjb21tZW5kYXRpb24gdXNpbmcgdGhlIFJlY29tbWVuZGVybGFiIHdlIGNvdmVyZWQgaW4gY2xhc3MgYW5kIGV4cGVyaW1lbnQgd2l0aCBBcHJpb3JpIEFsZ29yaXRobSBmb3IgQXNzb2NpYXRpb24gUnVsZXMgd2hpY2ggaXMgZGlmZmVyZW50IGZyb20gd2hhdCB3ZSBkaWQgaW4gY2xhc3MuIA0KDQpUaGUgdWx0aW1hdGUgZ29hbCBvZiB1c2luZyBib3RoIGFwcHJvYWNoZXMgaXMgdG8gY29tcGFyZSB0aGUgaXRlbXMgcmVjb21tZW5kZWQgdXNpbmcgYWxnb3JpdGhtIHNlbGVjdGVkIGZyb20gdGhlIFJlY29tbWVuZGVybGFiIGFuZCB0aGF0IG9mIEFwcmlvcmkgQXNzb2NpYXRpb24gUnVsZS4NCg0KIyBQcm9qZWN0IEhpZ2hsaWdodHMNCg0KUmVjb21tZW5kZXJsYWIgYWNjZXB0cyAyIHR5cGVzIG9mIHJhdGluZyBtYXRyaXggZm9yIG1vZGVsbGluZzoNCg0KUmVhbCByYXRpbmcgbWF0cml4IGNvbnNpc3Rpbmcgb2YgYWN0dWFsIHVzZXIgcmF0aW5ncywgd2hpY2ggcmVxdWlyZXMgbm9ybWFsaXNhdGlvbi4NCg0KQmluYXJ5IHJhdGluZyBtYXRyaXggY29uc2lzdGluZyBvZiAw4oCZcyBhbmQgMeKAmXMsIHdoZXJlIDHigJlzIGluZGljYXRlIGlmIHRoZSBwcm9kdWN0IHdhcyBwdXJjaGFzZWQuIFRoaXMgaXMgdGhlIG1hdHJpeCB0eXBlIG5lZWRlZCBmb3IgdGhlIGFuYWx5c2lzIGFuZCBpdCBkb2VzIG5vdCByZXF1aXJlIG5vcm1hbGl6YXRpb24uDQoNCjEuIEkgdXNlZCB0aGUgcmVjb21tZW5kZXJsYWIsIGFuIFIgcGFja2FnZSB3aGljaCBwcm92aWRlcyBhIGNvbnZlbmllbnQgZnJhbWV3b3JrIHRvIGV2YWx1YXRlIGFuZCBjb21wYXJlIHZhcmlvdXMgcmVjb21tZW5kYXRpb24gYWxnb3JpdGhtcyBhbmQgcXVpY2tseSBlc3RhYmxpc2ggdGhlIGJlc3Qgc3VpdGVkIGFwcHJvYWNoLg0KDQoyLiBJIGFycmFuZ2VkIHRoZSBwdXJjaGFzZSBoaXN0b3J5IGluIGEgcmF0aW5nIG1hdHJpeCwgd2l0aCBvcmRlcnMgaW4gcm93cyBhbmQgcHJvZHVjdHMgaW4gY29sdW1ucy4gVGhpcyBmb3JtYXQgaXMgb2Z0ZW4gY2FsbGVkIGEgdXNlcl9pdGVtIG1hdHJpeCBiZWNhdXNlIOKAnHVzZXJz4oCdIChlLmcuIGN1c3RvbWVycyBvciBvcmRlcnMpIHRlbmQgdG8gYmUgb24gdGhlIHJvd3MgYW5kIOKAnGl0ZW1z4oCdIChlLmcuIHByb2R1Y3RzKSBvbiB0aGUgY29sdW1ucy4NCg0KMy4gVGhlIHJlY29tbWVuZGVybGFiIGhhcyBhbiBhYmlsaXR5IHRvIGVzdGltYXRlIG11bHRpcGxlIGFsZ29yaXRobXMgYXQgYSB0aW1lLCBJIHdpbGwgY3JlYXRlIGEgbGlzdCB3aXRoIHRoZSBhbGdvcml0aG1zIGFuZCBjb25zaWRlciBzY2hlbWVzIHdoaWNoIGV2YWx1YXRlIG9uIGEgYmluYXJ5IHJhdGluZyBtYXRyaXguIA0KDQo0LiBJIGNvbXBhcmVkIGFuZCBldmFsdWF0ZWQgdGhlIHBlcmZvcm1hbmNlIG9mIHRoZSBhbGdvcml0aG1zIHVzaW5nIFJPQyBDdXJ2ZSwgUHJlY2lzaW9uL1JlY2FsbCwgUk1TRSwgTVNFIGFuZCBNQUUgYW5kIHVzZSB0aGUgYmVzdCBwZXJmb3JtaW5nIGFsZ29yaXRobSB0byBwcmVkaWN0IHRvcCBpdGVtcyBsaXN0IGZvciB0aGUgbmV3IGN1c3RvbWVyLg0KDQo1LiBGaW5hbGx5LCBJIGRlcGxveWVkIHRoZSBhbGdvcml0aG0gdXNpbmcgU2hpbnkgQXBwIGZyb20gUg0KDQojIEFib3V0IHRoZSBEYXRhDQoNClRoZSBkYXRhIGZvciB0aGlzIHByb2plY3QgY29tZXMgZnJvbSB0aGUgVUNJIE1hY2hpbmUgTGVhcm5pbmcgUmVwb3NpdG9yeSwgYW4gb25saW5lIGFyY2hpdmUgb2YgbGFyZ2UgZGF0YXNldHMgd2hpY2ggaW5jbHVkZXMgYSB3aWRlIHZhcmlldHkgb2YgZGF0YSB0eXBlcywgYW5hbHlzaXMgdGFza3MsIGFuZCBhcHBsaWNhdGlvbiBhcmVhcy4NCg0KSW4gdGhpcyBwcm9qZWN0IEkgdXNlZCB0aGUgT25saW5lIFJldGFpbCBkYXRhc2V0IGRvbmF0ZWQgdG8gVUNJIGluIDIwMTUgYnkgdGhlIFNjaG9vbCBvZiBFbmdpbmVlcmluZyBhdCBMb25kb24gU291dGggQmFuayBVbml2ZXJzaXR5LiANCg0KVGhpcyBkYXRhc2V0IGlzIGNvbW1vbiBhbmQgaGF2ZSBiZWVuIHVzZWQgaW4gbWFueSBNYXJrZXQgQmFza2V0IEFuYWx5c2lzOyB0aGUgZGF0YXNldCBjb250YWlucyB0cmFuc2FjdGlvbnMgb2NjdXJyaW5nIGJldHdlZW4gMDEvRGVjLzIwMTAgYW5kIDA5L0RlYy8yMDExIGZvciBhIFVLLWJhc2VkIGFuZCByZWdpc3RlcmVkIG9ubGluZSByZXRhaWwgY29tcGFueSBhbmQgY29udGFpbnMgNTQxLDkwOSBvYnNlcnZhdGlvbnMgd2l0aCBlaWdodCB2YXJpYWJsZXMuIA0KDQpUaGUgZGF0YSBpcyB0b28gbGFyZ2UgZm9yIG15IEdpdEh1YiBidXQgY2FuIGJlIGRvd25sb2FkZWQgZnJvbTogaHR0cDovL2FyY2hpdmUuaWNzLnVjaS5lZHUvbWwvbWFjaGluZS1sZWFybmluZy1kYXRhYmFzZXMvMDAzNTIvDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShwbHlyKQ0KbGlicmFyeShrYWJsZUV4dHJhKQ0KbGlicmFyeShyZWNvbW1lbmRlcmxhYikNCmxpYnJhcnkoZGF0YS50YWJsZSkgICAgICAgICAgIA0KbGlicmFyeShyZWFkeGwpICAgICAgICAgICAgICAgDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkoc2tpbXIpICAgICAgICAgICAgICAgIA0KbGlicmFyeShrbml0cikgICAgICAgICAgICAgICAgDQpsaWJyYXJ5KHRyZWVtYXApDQpsaWJyYXJ5KFJDb2xvckJyZXdlcikNCmxpYnJhcnkoYXJ1bGVzKQ0KbGlicmFyeShhcnVsZXNWaXopDQpgYGANCg0KDQojIyBMb2FkaW5nIHRoZSBPbmxpbmUgUmV0YWlsIGRhdGFzZXQNCg0KYGBge3J9DQoNCnJldGFpbCA8LSByZWFkX2V4Y2VsKCJDOi9Vc2Vycy9FbWFoYXl6X1Byby9EZXNrdG9wL0RhdGFfU2NpZW5jZS9EYXRhIDYxMi9XZWVrNi1GaW5hbCBQcm9qZWN0IFByb3Bvc2FsL09ubGluZSBSZXRhaWwueGxzeCIsdHJpbV93cyA9IFRSVUUpDQoNCmBgYA0KDQojIyBEYXRhIHN0cnVjdHVyZQ0KDQpgYGB7cn0NCnN0cihyZXRhaWwpDQpgYGANCg0KIyMgUmVtb3ZlIHRoZSBDYW5jZWxsZWQgVHJhbnNhY3Rpb24NCg0KYGBge3J9DQpyZXRhaWwgIDwtIHJldGFpbCAlPiUgDQogIGZpbHRlcighZ3JlcGwoIkMiLCByZXRhaWwkSW52b2ljZU5vKSkNCmBgYA0KDQojIyBFeGNsdWRpbmcgdGhlIG1pc3NpbmcgdmFsdWVzDQoNCmBgYHtyfQ0KcmV0YWlsIDwtIHJldGFpbCAlPiUgDQogIGZpbHRlcighaXMubmEoRGVzY3JpcHRpb24pKQ0KDQpyZXRhaWwgPC0gcmV0YWlsICU+JSANCiAgZmlsdGVyKCFpcy5uYShDdXN0b21lcklEKSkNCmBgYA0KDQojIyBSZW1vdmUgZHVwbGljYXRlIGl0ZW1zDQoNCkNyZWF0aW5nIGFuZCBkcm9wcGluZyB0aGUgdW5pcXVlIGlkZW50aWZpZXIgdG8gZmlsdGVyIHRoZSBkdXBsaWNhdGVzDQoNCmBgYHtyfQ0KcmV0YWlsIDwtIHJldGFpbCAlPiUgDQoNCiAgICBtdXRhdGUoSW5Ob19EZXNjID0gcGFzdGUoSW52b2ljZU5vLCBEZXNjcmlwdGlvbiwgc2VwID0gJyAnKSkgDQoNCg0KICAgIHJldGFpbCA8LSByZXRhaWxbIWR1cGxpY2F0ZWQocmV0YWlsJEluTm9fRGVzYyksIF0gJT4lIA0KICAgIHNlbGVjdCgtSW5Ob19EZXNjKQ0KYGBgDQoNCiMjIERhdGEgc3VtbWFyeQ0KDQpgYGB7cn0NCnN1bW1hcnkocmV0YWlsKQ0KYGBgDQoNCiMgUHJlLXByb2Nlc3NpbmcNCg0KKipDb252ZXJ0IGRhdGEgdG8gbnVtZXJpYyoqDQoNCmBgYHtyfQ0Kc2FwcGx5KHJldGFpbFsgLGMoJ0ludm9pY2VObycsJ0N1c3RvbWVySUQnKV0sIA0KICAgICAgICAgICAgICAgZnVuY3Rpb24oeCkgbGVuZ3RoKHVuaXF1ZSh4KSkpDQoNCnJldGFpbCA8LSByZXRhaWwgJT4lDQogIG11dGF0ZShEZXNjcmlwdGlvbiA9IGFzLmZhY3RvcihEZXNjcmlwdGlvbikpICU+JQ0KICBtdXRhdGUoQ291bnRyeSA9IGFzLmZhY3RvcihDb3VudHJ5KSkgJT4lIA0KICBtdXRhdGUoSW52b2ljZU5vID0gYXMubnVtZXJpYyhJbnZvaWNlTm8pKSAlPiUgDQogIG11dGF0ZShEYXRlID0gYXMuRGF0ZShJbnZvaWNlRGF0ZSkpICU+JSANCiAgbXV0YXRlKFRpbWUgPSBhcy5mYWN0b3IoZm9ybWF0KEludm9pY2VEYXRlLCIlSDolTTolUyIpKSkNCmBgYA0KDQojIyBFeHBsb3JpbmcgdGhlIGRhdGFzZXQNCg0KKipWaWV3IHRoZSBEaXN0cmlidXRpb25zKioNCg0KYGBge3J9DQp0cmVlbWFwKHJldGFpbCwNCiAgICAgICAgaW5kZXggICAgICA9IGMoIkNvdW50cnkiKSwNCiAgICAgICAgdlNpemUgICAgICA9ICJRdWFudGl0eSIsDQogICAgICAgIGFsZ29yaXRobSAgPSAicGl2b3RTaXplIiwNCiAgICAgICAgdGl0bGUgICAgICA9ICJUaGUgQ291bnRyeSB3aXRoIHRoZSBNb3N0IFB1cmNoYXNlIiwNCiAgICAgICAgcGFsZXR0ZSAgICA9ICJTZXQzIiwNCiAgICAgICAgYm9yZGVyLmNvbCA9ICJncmV5MjAiKQ0KYGBgDQoNCiMjIENyZWF0ZSB0aGUgUmF0aW5nIE1hdHJpeA0KDQpgYGB7cn0NCnJhdGluZ01hdCA8LSByZXRhaWwgJT4lDQogIHNlbGVjdChJbnZvaWNlTm8sIERlc2NyaXB0aW9uKSAlPiUgDQogIG11dGF0ZSh2YWx1ZSA9IDEpICU+JSANCiAgc3ByZWFkKERlc2NyaXB0aW9uLCB2YWx1ZSwgZmlsbCA9IDApICU+JQ0KICBzZWxlY3QoLUludm9pY2VObykgJT4lIA0KICBhcy5tYXRyaXgoKSAlPiUgDQogIGFzKCJiaW5hcnlSYXRpbmdNYXRyaXgiKQ0KcmF0aW5nTWF0DQpgYGANCg0KIyBBbGdvcml0aG1zLSBNb2RlbGluZw0KDQpTZXQgdXAgTGlzdCBvZiBBbGdvcml0aG1zDQoNCmBgYHtyLHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQpTY2hlbWUgPC0gcmF0aW5nTWF0ICU+JSANCiAgZXZhbHVhdGlvblNjaGVtZShtZXRob2QgPSAiY3Jvc3MiLA0KICAgICAgICAgICAgICAgICAgIGsgICAgICA9IDUsIA0KICAgICAgICAgICAgICAgICAgIHRyYWluICA9IDAuOCwgIA0KICAgICAgICAgICAgICAgICAgIGdpdmVuICA9IC0xKQ0KU2NoZW1lDQpgYGANCg0KYGBge3J9DQpNb2RlbEFsZ29yaXRobXMgPC0gbGlzdCgNCiAgIkFzc29jaWF0aW9uIiA9IGxpc3QobmFtZSAgPSAiQVIiLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHBhcmFtID0gbGlzdChzdXBwID0gMC4wMSwgY29uZiA9IDAuOCkpLA0KICAiVXNlckJhc2VkIiA9IGxpc3QobmFtZT0iVUJDRiIsIHBhcmFtPWxpc3QobWV0aG9kPSJDb3NpbmUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5uPTUwMCkpLA0KICAiSXRlbUJhc2VkIiA9IGxpc3QobmFtZT0iSUJDRiIsIHBhcmFtPWxpc3QoayA9IDUpKQ0KICANCiAgKQ0KYGBgDQoNCiMjIEVzdGltYXRlIHRoZSBNb2RlbHMNCg0KVXNpbmcgbj1jKDEsIDUsIDEwLCAxNSwgMjAsIDI1KSB0byBldmFsdWF0ZSBwcm9kdWN0IHJlY29tbWVuZGF0aW9ucw0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCk1vZGVsUmVzdWx0cyA8LSBldmFsdWF0ZShTY2hlbWUsIE1vZGVsQWxnb3JpdGhtcywgbj1jKDEsIDUsIDEwLCAxNSwgMjAsIDI1KSkNCmBgYA0KDQpJdCBzZWVtcyB0aGUgSUJDRiB0b29rIGxvbmdlciB0byBlc3RpbWF0ZQ0KDQojIENvbXByaXNvbiBvZiB0aGUgTW9kZWwgQWNjdXJhY3kNCg0KIyMgUk9DIGN1cnZlDQoNCmBgYHtyfQ0KcGxvdChNb2RlbFJlc3VsdHMsIGFubm90YXRlID0gVFJVRSwgbGVnZW5kID0gInRvcGxlZnQiKQ0KdGl0bGUoIlJPQyBDdXJ2ZSIpDQpgYGANCg0KIyMgUHJlY2lzaW9uIGFuZCBSZWNhbGwNCg0KYGBge3J9DQpwbG90KE1vZGVsUmVzdWx0cywgInByZWMvcmVjIiwgYW5ub3RhdGUgPSBUUlVFKSAjIHByZWNpc2lvbiBhbmQgcmVjYWxsDQp0aXRsZSgiUHJlY2lzaW9uIGFuZCBSZWNhbGwiKQ0KYGBgDQoNCg0KQ2xhc3NpZmljYXRpb24gbW9kZWxzIHBlcmZvcm1hbmNlIGNhbiBiZSBjb21wYXJlZCB1c2luZyB0aGUgUk9DIGN1cnZlLCB3aGljaCBwbG90cyB0aGUgdHJ1ZSBwb3NpdGl2ZSByYXRlIChUUFIpIGFnYWluc3QgdGhlIGZhbHNlIHBvc2l0aXZlIHJhdGUgKEZQUikuDQoNClRoZSBpdGVtLWJhc2VkIGNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nIG1vZGVsIGlzIHRoZSB3aW5uZXIgYXMgaXQgYWNoaWV2ZXMgdGhlIGhpZ2hlc3QgVFBSIGZvciBhbnkgZ2l2ZW4gbGV2ZWwgb2YgRlBSLiANCg0KVGhpcyBtZWFucyB0aGF0IHRoZSBtb2RlbCBpcyBwcm9kdWNpbmcgdGhlIGhpZ2hlc3QgbnVtYmVyIG9mIHJlbGV2YW50IHJlY29tbWVuZGF0aW9ucyBrbm93biBhcyBUcnVlIFBvc2l0aXZlcyhUUCkpIGZvciB0aGUgc2FtZSBsZXZlbCBvZiBub24tcmVsZXZhbnQgcmVjb21tZW5kYXRpb25zIGtub3duIGFzIEZhbHNlIFBvc2l0aXZlcyhGUCkpLg0KDQojIFRyYWluaW5nLCBUZXN0aW5nIGFuZCBFdmFsdWF0aW9uIERhdGEgc2V0cw0KDQoqKlByZXBhcmUgdHJhaW5pbmcgZGF0YXNldCoqDQoNCmBgYHtyfQ0KdHJhaW4gPC0gZ2V0RGF0YShTY2hlbWUsICJ0cmFpbiIpDQpgYGANCg0KKipQcmVwYXJlIHRlc3Rpbmcgc2V0KioNCg0KYGBge3J9DQp0ZXN0IDwtIGdldERhdGEoU2NoZW1lLCAia25vd24iKQ0KYGBgDQoNCioqUHJlcGFyZSBldmFsdWF0aW9uIHNldCoqDQoNCmBgYHtyfQ0KZXZhbHVhdGlvbiA8LSBnZXREYXRhKFNjaGVtZSwgInVua25vd24iKQ0KYGBgDQoNCiMgUmVjb21tZW5kZXIgU3lzdGVtDQoNCiMjIEJlc3QgUGVyZm9ybWluZyBNb2RlbCAtIEl0ZW0gQmFzZWQgDQoNCmBgYHtyfQ0KSXRlbW1vZGVsIDwtIFJlY29tbWVuZGVyKHRyYWluLCBtZXRob2QgPSAiSUJDRiIsICANCiAgICAgICAgICAgICAgICAgICAgICAgcGFyYW0gPSBsaXN0KGsgPSA1KSkNCmBgYA0KDQojIyMgTWFraW5nIFByZWRpY3Rpb25zIHdpdGggbmV3ZGF0YSA9IHRlc3QNCg0KYGBge3Isd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCkl0ZW1wcmVkIDwtIHByZWRpY3QoSXRlbW1vZGVsLCBuZXdkYXRhID0gdGVzdCkNCg0KYGBgDQoNCiMjIyBNb2RlbCBFdmFsdWF0aW9uDQoNCmBgYHtyfQ0KSXRlbWFjY3VyYWN5IDwtIGNhbGNQcmVkaWN0aW9uQWNjdXJhY3koSXRlbXByZWQsIGV2YWx1YXRpb24sIGdpdmVuPTEwKQ0Ka2FibGUoSXRlbWFjY3VyYWN5LGNhcHRpb24gPSAiUGVyZm9ybWFuY2UgTWV0cmljcyIpIA0KYGBgDQoNCiMjIyBSZWNvbW1lbmRlZCBJdGVtcyBmcm9tIHRoZSBQcmVkaWN0aW9uIA0KDQpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCnJlY29tbWVuZGVkSXRlbXMgPC0gYXMoSXRlbXByZWQsICdsaXN0JykNCg0KcmVjb21tZW5kZWRJdGVtcyA8LSBkby5jYWxsKCJyYmluZCIsIGxhcHBseShyZWNvbW1lbmRlZEl0ZW1zLCBhcy5kYXRhLmZyYW1lKSkNCg0Kd3JpdGUuY3N2KHJlY29tbWVuZGVkSXRlbXMsDQogICAgICBmaWxlID0gIkl0ZW1CYXNlZCBSZWNvbW1lbmRhdGlvbi5jc3YiLA0KICAgICAgcm93Lm5hbWVzID0gVFJVRSwgDQogICAgICBzZXAgPSAnLCcsIA0KICAgICAgY29sLm5hbWVzID0gVFJVRQ0KICAgICApDQoNCmBgYA0KDQoNCk5vdyB3ZSBjYW4gc2VlIGEgbGlzdCBvZiB0aGUgcmVjb21tZW5kZWQgaXRlbXMgYXMgcHJvdmlkZWQgYnkgSXRlbS1CYXNlZCBjb2xsYWJvcmF0aXZlIGZpbHRlci4gSW4gdGhlIHNlY29uZCBwYXJ0IG9mIHRoaXMgcHJvamVjdCwgSSB3aWxsIGV4cGVyaW1lbnQgd2l0aCBBc3NvY2lhdGlvbiBSdWxlcyB1c2luZyBBcHJpb3JpIHRvIGNvbXBhcmUgdGhlIHJlY29tbWVuZGVkIGl0ZW1zIGJhc2VkIG9uIHN1cHBvcnQsIGNvbmZpZGVuY2UgYW5kIGxpZnQuDQoNCiMgVXNpbmcgQXNzb2NpYXRpb24gUnVsZXMNCg0KIyMgVHJhbnNhY3Rpb24gRGF0YQ0KDQpDcmVhdGUgYW5kIHZpZXcgdGhlIHRyYW5zYWN0aW9uIGRhdGEgc2luY2UgQXByaW9yaSBhbGdvcml0aG0gb25seSB3b3JrcyB3aXRoIHRyYW5zYWN0aW9uIG9iamVjdA0KDQpgYGB7cn0NCnJldGFpbHRyYW5zYWN0aW9uIDwtIGRkcGx5KHJldGFpbCxjKCJJbnZvaWNlTm8iLCJEYXRlIiksDQogICAgICAgICAgICAgICAgICAgICAgIGZ1bmN0aW9uKGRmMSlwYXN0ZShkZjEkRGVzY3JpcHRpb24sDQogICAgICAgICAgICAgICAgICAgICAgIGNvbGxhcHNlID0gIiwiKSkNCnJldGFpbHRyYW5zYWN0aW9uDQpgYGANCg0KIyMjIFRyYW5zYWN0aW9uIEJhc2tldCBmb3IgSXRlbXMNCg0KQ2FzdCBJbnZvaWNlIGFuZCBEYXRlIHRvIE5VTEwgc2luY2UgdGhleSdyZSBub3QgcmVxdWlyZWQgYW5kIGNyZWF0ZSBpdGVtcyBCYXNrZXQNCg0KYGBge3J9DQpyZXRhaWx0cmFuc2FjdGlvbiRJbnZvaWNlTm8gPC0gTlVMTA0KcmV0YWlsdHJhbnNhY3Rpb24kRGF0ZSA8LSBOVUxMDQpjb2xuYW1lcyhyZXRhaWx0cmFuc2FjdGlvbikgPC0gYygiaXRlbXMiKQ0KcmV0YWlsdHJhbnNhY3Rpb24NCmBgYA0KDQojIyMgVHJhbnNhY3Rpb24gT2JqZWN0DQoNClNhdmUgdGhlIHRyYW5zYWN0aW9uIGRhdGEgYXMgY3N2IGFuZCB1cGxvYWQgaXQgdG8gY3JlYXRlIHRyYW5zYWN0aW9uIE9iamVjdC4NCg0KYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0V9DQp3cml0ZS5jc3YocmV0YWlsdHJhbnNhY3Rpb24sIkM6L1VzZXJzL0VtYWhheXpfUHJvL0Rlc2t0b3AvRGF0YV9TY2llbmNlL0RhdGEgNjEyL1dlZWs3LUZpbmFsIFByb2plY3QgU3VibWlzc2lvbi9yZXRhaWx0cmFuc2FjdGlvbnMuY3N2IiwgcXVvdGUgPSBGQUxTRSwgcm93Lm5hbWVzID0gRkFMU0UpDQoNCnJldGFpbHRyYW5zIDwtIHJlYWQudHJhbnNhY3Rpb25zKCJDOi9Vc2Vycy9FbWFoYXl6X1Byby9EZXNrdG9wL0RhdGFfU2NpZW5jZS9EYXRhIDYxMi9XZWVrNy1GaW5hbCBQcm9qZWN0IFN1Ym1pc3Npb24vcmV0YWlsdHJhbnNhY3Rpb25zLmNzdiIsIGZvcm1hdCA9ICdiYXNrZXQnLCBybS5kdXBsaWNhdGVzID0gRkFMU0UsIHNlcD0nLCcpDQpyZXRhaWx0cmFucw0KYGBgDQoNCiMjIFRyYW5zYWN0aW9uIFN1bW1hcnkgDQoNCmBgYHtyfQ0Kc3VtbWFyeSAocmV0YWlsdHJhbnMpDQpgYGANCg0KVGhlcmUgYXJlIDE4NTM3IHRyYW5zYWN0aW9ucyAocm93cykgYW5kIDc3MjUgaXRlbXMgKGNvbHVtbnMpLiBOb3RlIHRoYXQgNzcyNSBpcyB0aGUgcHJvZHVjdCBkZXNjcmlwdGlvbnMgaW52b2x2ZWQgaW4gdGhlIGRhdGFzZXQgYW5kIDE4NTM3IHRyYW5zYWN0aW9ucyBhcmUgY29sbGVjdGlvbnMgb2YgdGhlc2UgaXRlbXMuDQoNCkRlbnNpdHkgdGVsbHMgdGhlIHBlcmNlbnRhZ2Ugb2Ygbm9uLXplcm8gY2VsbHMgaW4gYSBzcGFyc2UgbWF0cml4LiBXZSBjYW4gc2F5IGl0IGFzIHRoZSB0b3RhbCBudW1iZXIgb2YgaXRlbXMgdGhhdCBhcmUgcHVyY2hhc2VkIGRpdmlkZWQgYnkgYSBwb3NzaWJsZSBudW1iZXIgb2YgaXRlbXMgaW4gdGhhdCBtYXRyaXguIA0KDQojIyBJdGVtIEZyZXF1ZW5jeSBQbG90DQoNCkkgY2FuIGdlbmVyYXRlIGFuIGl0ZW1GcmVxdWVuY3lQbG90IHRvIGNyZWF0ZSBhbiBpdGVtIEZyZXF1ZW5jeSBCYXIgUGxvdCB0byB2aWV3IHRoZSBkaXN0cmlidXRpb24gb2Ygb2JqZWN0cyBiYXNlZCBvbiBpdGVtTWF0cml4IHVzaW5nIGFic29sdXRlIGl0ZW0gZnJlcXVlbmN5LiBIZXJlLCBJIHdpbGwgdmlldyB0aGUgdG9wIDIwIGl0ZW1zLg0KDQpgYGB7cn0NCml0ZW1GcmVxdWVuY3lQbG90KHJldGFpbHRyYW5zLHRvcE49MjAsdHlwZT0iYWJzb2x1dGUiLGNvbD1icmV3ZXIucGFsKDgsJ1Bhc3RlbDInKSwgbWFpbj0iQWJzb2x1dGUgSXRlbSBGcmVxdWVuY3kgUGxvdCIpDQpgYGANCg0KIyBDb25zdHJ1Y3RpbmcgdGhlIG1vZGVsLSBBc3NvY2lhdGlvbiBSdWxlcw0KDQpOZXh0IHN0ZXAgaXMgdG8gbWluZSB0aGUgcnVsZXMgdXNpbmcgdGhlIEFQUklPUkkgYWxnb3JpdGhtLiBUaGUgZnVuY3Rpb24gYXByaW9yaSgpIGlzIGZyb20gcGFja2FnZSBhcnVsZXMuDQoNClVzaW5nIE1pbiBTdXBwb3J0IGFzIDAuMDAxLCBjb25maWRlbmNlIGFzIDAuOC4NCg0KIyMgQXNzb2NpYXRpb24gUnVsZXMNCg0KKipDb250cm9sIFRoZSBOdW1iZXIgT2YgUnVsZXMqKg0KDQpJIGNhbiBhZGp1c3QgdGhlIG1heGxlbiwgc3VwcCBhbmQgY29uZiBhcmd1bWVudHMgaW4gdGhlIGFwcmlvcmkgZnVuY3Rpb24gdG8gY29udHJvbCB0aGUgbnVtYmVyIG9mIHJ1bGVzIGdlbmVyYXRlZC4NCg0K4oCiCVRvIGdldCDigJhzdHJvbmfigJggcnVsZXMsIEkgY2FuIGluY3JlYXNlIHRoZSB2YWx1ZSBvZiDigJhjb25m4oCZIHBhcmFtZXRlciwgaGVyZSB3ZSBhcmUgdXNpbmcgMC44IHdoaWNoIGlzIGhpZ2ggIA0KICBhdCA4MCUuDQrigKIJVG8gZ2V0IOKAmGxvbmdlcuKAmCBydWxlcywgSSBjYW4gaW5jcmVhc2Ug4oCYbWF4bGVu4oCZLCBoZXJlIHdlIGFyZSB1c2luZyB0aGUgbWluaW11bSBudW1iZXIgb2YgbGVuZ3RoICgyKSB0aGF0IA0KICBjYW4gZ2VuZXJhdGUgYSBydWxlIHdoaWNoIGlzIHRoZSBmaXJzdCBhdHRlbXB0IGZvciB0aGlzIHRyYW5zYWN0aW9uIGRhdGEuDQoNCmBgYHtyfQ0KcmV0YWlscnVsZXMgPC0gYXByaW9yaShyZXRhaWx0cmFucywgcGFyYW1ldGVyID0gbGlzdChzdXBwID0gMC4wMDEsIGNvbmYgPSAwLjgsIG1heGxlbiA9IDIpKQ0Kc3VtbWFyeShyZXRhaWxydWxlcykNCmBgYA0KDQpBcHJpb3JpIGNyZWF0ZWQgMTEzIHJ1bGVzIGZvciBtYXhsZW4gPSAyLCBJIGRlY2lkZWQgdG8gdXNlIG1heGxlbiA9IDIgYmVjYXVzZSBncmVhdGVyIHRoYW4gMiB3aWxsIGNyZWF0ZSBydWxlcyBpbiB0aGUgdGhvdXNhbmRzIHdoaWNoIEkgd2FudCB0byBhdm9pZC4NCg0KVGhlIG1heGltdW0gc3VwcG9ydCBpcyAwLjAyMjI4MCwgdGhlIGF2ZXJhZ2UgY29uZmlkZW5jZSBpcyAwLjkxODcgYW5kIG1heGltdW0gQ29uZmlkZW5jZSBpcyAxLiBUaGUgYXZlcmFnZSBsaWZ0IGlzIDE5My4zNCBhbmQgdGhlIG1heGltdW0gbGlmdCBpcyA1OTcuOTcNCg0KIyMjIExldCdzIHZpZXcgdGhlIFRvcCAxMCBydWxlcw0KDQpgYGB7cn0NCmluc3BlY3QocmV0YWlscnVsZXNbMToxMF0pDQpgYGANCg0KIyMjIFRoZSBUb3AgMjAgTGlmdCBSdWxlcw0KDQpJIHdpbGwgYWxzbyB0cnkgdG8gcmVtb3ZlIHJlZHVuZGFudCBydWxlcw0KDQpgYGB7cn0NCnJldGFpbHJ1bGVzX0xpZnQgPC0gc29ydCAocmV0YWlscnVsZXMsIGJ5ID0gImxpZnQiLCBkZWNyZWFzaW5nPVRSVUUpDQpHcm9jZXJpZXMucnVsZXNfTGlmdCA8LSByZXRhaWxydWxlc19MaWZ0WyFpcy5yZWR1bmRhbnQocmV0YWlscnVsZXNfTGlmdCldDQoNCiMgc2hvdyB0aGUgc3VwcG9ydCwgbGlmdCBhbmQgY29uZmlkZW5jZSBmb3IgdGhlIHJ1bGVzDQppbnNwZWN0KGhlYWQocmV0YWlscnVsZXNfTGlmdCwgMjApKQ0KDQp3cml0ZShyZXRhaWxydWxlc19MaWZ0LCANCiAgICAgIGZpbGUgPSAiQXByaW9yaSBSZWNvbW1lbmRlZCBJdGVtcy5jc3YiLA0KICAgICAgc2VwID0gIiwiLA0KICAgICAgcXVvdGUgPSBUUlVFLA0KICAgICAgcm93Lm5hbWVzID0gRkFMU0UNCiAgICAgICkNCg0KDQpgYGANCg0KVGhlIHJ1bGVzIHdpdGggY29uZmlkZW5jZSBvZiAxIGFzIHNlZW4gYWJvdmUgaW1wbGllcyB0aGF0LCB3aGVuZXZlciB0aGUgTEhTIGl0ZW0gd2FzIHB1cmNoYXNlZCwgdGhlIFJIUyBpdGVtIHdhcyBhbHNvIHB1cmNoYXNlZCAxMDAlIG9mIHRoZSB0aW1lLg0KDQpBIHJ1bGUgd2l0aCBhIGxpZnQgb2YgNTk3IGFzIHNlZW4gYWJvdmUgaW1wbGllcyB0aGF0LCB0aGUgaXRlbXMgaW4gTEhTIGFuZCBSSFMgYXJlIDU5NyB0aW1lcyBtb3JlIGxpa2VseSB0byBiZSBwdXJjaGFzZWQgdG9nZXRoZXIgY29tcGFyZWQgdG8gdGhlIHB1cmNoYXNlcyB3aGVuIHRoZXkgYXJlIGFzc3VtZWQgdG8gYmUgdW5yZWxhdGVkLg0KDQojIyMgUGxvdGluZyB0aGUgcnVsZXMNCg0KUGxvdHRpbmcgYWxsIHRoZSAxMTMgcnVsZXMgd2lsbCBiZSB0b28gbXVjaCwgc28gSSB3aWxsIHN1YnNldCB0aGUgcnVsZXMgd2l0aCBjb25maWRlbmNlIG9mIGF0IGxlYXN0IDkwJS4NCg0KYGBge3J9DQpyZXRhaWxzdWJydWxlczwtcmV0YWlscnVsZXNfTGlmdFtxdWFsaXR5KHJldGFpbHJ1bGVzX0xpZnQpJGNvbmZpZGVuY2U+MC45XQ0KcGxvdChyZXRhaWxzdWJydWxlcykNCmBgYA0KDQpUaGUgY2hhcnQgc2hvd3MgdGhlIHNjYXR0ZXIgb2YgdGhlIHJlZHVjZWQgbnVtYmVyIG9mIHJ1bGVzICg2NikuDQoNCkxldCdzIHNlbGVjdCAyMCBydWxlcyBoYXZpbmcgdGhlIGhpZ2hlc3QgY29uZmlkZW5jZS4NCg0KKipUb3AgMjAgUnVsZXMqKg0KDQpgYGB7cn0NCnRvcHJldGFpbHN1YnJ1bGVzIDwtIGhlYWQocmV0YWlsc3VicnVsZXMsIG4gPSAyMCwgYnkgPSAiY29uZmlkZW5jZSIpDQpgYGANCg0KIyMjIEdyYXBoIG9mIFRvcCAyMCBSdWxlcw0KDQpgYGB7cn0NCnBsb3QodG9wcmV0YWlsc3VicnVsZXMsIG1ldGhvZCA9ICJncmFwaCIsIGxheW91dD1pZ3JhcGg6OmluX2NpcmNsZSgpKQ0KYGBgDQoNCiMjIyBJbnRlcmFjdGl2ZSBHcmFwaCBVc2luZyBQbG90bHkNCg0KVXNpbmcgdGhlIGludGVyYWN0aXZlIHBsb3QsIEkgY2FuIHNlZSB0aGUgcmVjb21tZW5kZWQgaXRlbXMgZnJvbSB0aGUgTEhTIGFuZCB0aGUgbWF0Y2hpbmcgaXRlbXMgb24gdGhlIFJIUyB3aXRoIHRoZSByZXNwZWN0aXZlIHN1cHBvcnQsIGxpZnQgYW5kIGNvbmZpZGVuY2UuDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFfQ0KcGxvdGx5X2FydWxlcyh0b3ByZXRhaWxzdWJydWxlcykNCmBgYA0KDQpJIGNhbiBhbHNvIGludGVyYWN0aXZlbHkgc2VsZWN0IGNlcnRhaW4gcnVsZXMgdG8gdmlldyB0aGUgcmVjb21tZW5kZWQgaXRlbXMNCg0KYGBge3J9DQpwbG90KHRvcHJldGFpbHN1YnJ1bGVzLCBtZXRob2QgPSAiZ3JhcGgiLCAgZW5naW5lID0gImh0bWx3aWRnZXQiKQ0KYGBgDQoNCiMgQ29uY2x1c2lvbg0KDQpUaGUgcHVycG9zZSBvZiB0aGlzIHByb2plY3Qgd2FzIHRvIGJ1aWxkIGEgcmVjb21tZW5kZXIgc3lzdGVtIGFuZCB0byBwcmFjdGljZSB3b3JraW5nIHdpdGggYXByaW9yaSBhc3NvY2lhdGlvbiBydWxlcyB0byByZWNvbW1lbmQgaXRlbXMgZm9yIHBvc3NpYmxlIHB1cmNoYXNlcy4NCg0KRm9yIHRoZSBnaXZlbiBkYXRhIHNldCwgdGhlIEl0ZW0tYmFzZWQgTW9kZWwgd2l0aGluIHJlY29tbWVuZGVybGFiIHByb3ZpZGVkIHRoZSBiZXN0IHJlc3VsdHMuIEl0IGlzIHdvcnRoIHJlbWVtYmVyaW5nIHRoYXQgdGhlIGNob2ljZSBvZiB0aGUgcmVjb21tZW5kZXIgYWxnb3JpdGhtIGRlcGVuZHMgb24gdGhlIGNvbnRleHQuIFRoZSBhY2N1cmFjeSBvZiB0aGUgSXRlbS1iYXNlZCBtb2RlbCBpcyBhY2NlcHRhYmxlIGNvbXBhcmVkIHRvIHRoZSBvdGhlciBhbGdvcml0aG1zIGNvbnNpZGVyZWQuDQoNCkluIGFkZGl0aW9uIHRvIHRoZSBtZXRob2RzIHByb3ZpZGVkIHdpdGhpbiB0aGUgcmVjb21tZW5kZXJsYWIgbGlicmFyeSwgdGhlcmUgYXJlIG90aGVyIHN1cGVydmlzZWQgYW5kIHVuc3VwZXJ2aXNlZCBtYWNoaW5lIGxlYXJuaW5nIGFsZ29yaXRobSBhcyB3ZWxsIGFzIG5ldXRyYWwgbmV0d29yay1kcml2ZW4gZGVlcCBsZWFybmluZyBhbGdvcml0aG1zIHRoYXQgY291bGQgYmUgdXNlZCB0byBidWlsZCByZWNvbW1lbmRhdGlvbiBzeXN0ZW1zLiANCg0KSG93ZXZlciwgSSB1c2VkIHRoZSB1bnN1cGVydmlzZWQgQXByaW9yaSBBbGdvcml0aG0gZm9yIEFzc29jaWF0aW9uIHJ1bGVzIGFuZCB3YXMgYWJsZSB0byBvYnRhaW4gdGhlIHJlY29tbWVuZGVkIGl0ZW1zLiBGb3IgZWFjaCBnaXZlbiBydWxlLCB0aGUgaXRlbXMgb24gdGhlIExIUyBoYXZlIGEgbWF0Y2hpbmcgaXRlbXMgb24gdGhlIFJIUyBpbmRpY2F0aW5nIHRoZSBjb25maWRlbmNlIGxldmVsIHRvIHdoaWNoIHRoZXNlIGl0ZW1zIHdlcmUgcHVyY2hhc2VkIHRvZ2V0aGVyLiBUaGUgcmVjb21tZW5kZWQgaXRlbXMgZm9yIHRvcCAyMCBydWxlcyB3aXRoIHRoZSBjb3JyZXNwb25kaW5nIHN1cHBvcnQsIGxpZnQgYW5kIGNvbmZpZGVuY2UgYXJlIHByaW50ZWQgYW5kIHNhdmVkIGFzIGEgY3N2IGZpbGUgdGhhdCBpcyBzdWJtaXR0ZWQgd2l0aCB0aGlzIHByb2plY3QuDQoNCkZpbmFsbHksIEkgY3JlYXRlZCBhbiBpbnRlcmFjdGl2ZSBncmFwaCB0byBzZWUgdGhlIHJlY29tbWVuZGVkIGl0ZW1zIGZyb20gdGhlIExIUyBhbmQgdGhlIG1hdGNoaW5nIGl0ZW1zIG9uIHRoZSBSSFMgd2l0aCB0aGUgcmVzcGVjdGl2ZSBzdXBwb3J0LCBsaWZ0IGFuZCBjb25maWRlbmNlLiBUaGUgc2Vjb25kIGludGVyYWN0aXZlIGdyYXBoIHNob3dzIHdoZXJlIEkgY2FuIHNlbGVjdCBhbnkgcnVsZSBhbmQgdmlldyB0aGUgcmVjb21tZW5kZWQgaXRlbXMuDQoNCg0KIyBSZWZlcmVuY2VzDQoNCkJyZWVzZSBKUywgSGVja2VybWFuIEQsIEthZGllIEMgKDE5OTgpLiAiRW1waXJpY2FsIEFuYWx5c2lzIG9mIFByZWRpY3RpdmUgQWxnb3JpdGhtcyBmb3IgQ29sbGFib3JhdGl2ZSBGaWx0ZXJpbmcuIiBJbiBVbmNlcnRhaW50eSBpbiBBcnRpZmljaWFsIEludGVsbGlnZW5jZS4gUHJvY2VlZGluZ3Mgb2YgdGhlIEZvdXJ0ZWVudGggQ29uZmVyZW5jZSwgcHAuIDQzLTUyLg0KDQpKYWJlZW4sIEguICgyMDE4KS4gTWFya2V0IEJhc2tldCBBbmFseXNpcyB1c2luZyBSLiBSZXRyaWV2ZWQgZnJvbSBodHRwczovL3d3dy5kYXRhY2FtcC5jb20vY29tbXVuaXR5L3R1dG9yaWFscy9tYXJrZXQtYmFza2V0LWFuYWx5c2lzLXINCg0KS29oYXZpLCBSb24gKDE5OTUpLiAiQSBzdHVkeSBvZiBjcm9zcy12YWxpZGF0aW9uIGFuZCBib290c3RyYXAgZm9yIGFjY3VyYWN5IGVzdGltYXRpb24gYW5kIG1vZGVsIHNlbGVjdGlvbiIuIFByb2NlZWRpbmdzIG9mIHRoZSBGb3VydGVlbnRoIEludGVybmF0aW9uYWwgSm9pbnQgQ29uZmVyZW5jZSBvbiBBcnRpZmljaWFsIEludGVsbGlnZW5jZSwgcHAuIDExMzctMTE0My4NCg0KS29yZW4sIFkuLCBCZWxsLCBSLiwgJiBWb2xpbnNreSwgQy4gKDIwMDkpLiBNYXRyaXggRmFjdG9yaXphdGlvbiBUZWNobmlxdWVzIGZvciBSZWNvbW1lbmRlciBTeXN0ZW1zLiBDb21wdXRlciwgNDIoOCksIDMw4oCTMzcuIGh0dHBzOi8vZG9pLm9yZy8xMC4xMTA5L01DLjIwMDkuMjYzDQoNCk1pY2hhZWwgSGFoc2xlciAoMjAxNikuIHJlY29tbWVuZGVybGFiOiBBIEZyYW1ld29yayBmb3IgRGV2ZWxvcGluZyBhbmQgVGVzdGluZyBSZWNvbW1lbmRhdGlvbiBBbGdvcml0aG1zLCBSIHBhY2thZ2UuIGh0dHBzOi8vQ1JBTi5SLXByb2plY3Qub3JnL3BhY2thZ2U9cmVjb21tZW5kZXJsYWINCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQo=