🔎 How to download & run the codes?

All the source codes of the KFC procedure are available here . To run the codes, you can clone the repository directly or simply load the R script source file from the repository using devtools package in Rstudio as follow:

  1. Install devtools package using command:

    install.packages("devtools")

  2. Loading the source codes from GitHub repository using source_url function by:

    devtools::source_url("https://raw.githubusercontent.com/hassothea/KFC-Procedure/master/KFCRegressor.R")


✎ Note: All codes contained in this Rmarkdown are built with recent version of (version \(>\) 4.1, available here) and Rstudio (version > 2022.02.2+485, available here). Note also that the code chucks are hidden by default.

To see the codes, you can:


1 KFC procedure & important packages

1.1 KFC procedure

KFC procedure is a three-step methodology which puts together clustering and consensual aggregation methods for building predictions in supervised learning problems. The procedure is inspired by many real-life prediction problems when the in The three steps of the procedure are:

  • Step K : \(K\)-means clustering algorithm is implemented on the input data using several options of Bregman divergences \(({\cal B}_j)_{j=1}^M\) (\(M\) is the number of total divergences used), therefore, the input data is partitioned into many different structures, according to the property of each Bregman divergence.
  • Step F : For a partition structure given by a divergence \({\cal B}_j\), we fit simple models (linear, for example) on all the clusters of the obtained partition. Then, the collection \({\cal M}_j=\{{\cal M}_{j,k}\}_{k=1}^K\) of these local models is called candidate model, corresponding to the Bregman divergence \({\cal B}_j\). At the end of this step, several candidate models are constructed.
  • Step C : This step aggregates the obtained candidate models using consensual aggregation methods studied in Has (2021) or Fischer and Mougeot (2019).

The figure above represents the summary of KFC procedure


🧾 Remark.1: The prediction of any observation \(x\) given by a candidate model \({\cal M}_j\) is done in two simple steps:

  1. \(x\) is classified into one of the obtained clusters using the corresponding divergence \({\cal B}_j\), i.e., \[x\in{\cal C}_{k^*} \Leftrightarrow {\cal B}_j(c_{k^*},x)=\inf_{1\leq k\leq K}{\cal B}_j(c_k,x)\] where \(\{c_1,...,c_K\}_{k=1}^K\) are the centroids of the corresponding clusters \(\{C_1,...,C_K\}\).

  2. The prediction of \(x\) is given by the corresponding local model \({\cal M}_{j,k^*}\) defined on cluster \(k^*\), i.e., \({\cal M}_j(x)={\cal M}_{j,k^*}(x)\).


1.2 Important packages

We prepare all the necessary tools for this Rmarkdown. The pacman package allows us to load (if exists) or install (if does not exist) any available packages from The Comprehensive R Archive Network (CRAN) of .

# Check if package "pacman" is already installed 

lookup_packages <- installed.packages()[,1]
if(!("pacman" %in% lookup_packages))
  install.packages("pacman")


# To be installed or loaded
pacman::p_load(magrittr)
pacman::p_load(tidyverse)

## package for "generateMachines"
pacman::p_load(tree)
pacman::p_load(glmnet)
pacman::p_load(randomForest)
pacman::p_load(FNN)
pacman::p_load(xgboost)
pacman::p_load(keras)
pacman::p_load(pracma)
pacman::p_load(latex2exp)
pacman::p_load(plotly)
pacman::p_load(parallel)
pacman::p_load(foreach)
pacman::p_load(doParallel)
rm(lookup_packages)

2 Bregman divergences (BD)

Definition Let \(\phi:\mathcal{C}\rightarrow\mathbb{R}\) be a strictly convex and continuously differentiable function defined on a measurable convex subset \(\mathcal{C}\subset\mathbb{R}^d\). Let \(int(\mathcal{C})\) denote its relative interior. A Bregman divergence indexed by \(\phi\) is a dissimilarity measure \(d_{\phi}:\mathcal{C}\times int(\mathcal{C})\rightarrow\mathbb{R}\) defined for any pair \((x,y)\in \mathcal{C}\times int(\mathcal{C})\) by, \[\begin{equation} \label{eq:1.10} d_{\phi}(x,y)=\phi(x)-\phi(y)-\langle x-y,\nabla\phi(y)\rangle \end{equation}\] where \(\nabla\phi(y)\) denotes the gradient of \(\phi\) computed at a point \(y\in int(\mathcal{C})\). A Bregman divergence is not necessarily a metric as it may not be symmetric and the triangular inequality might not be satisfied.

This section defines all the Bregman divergences used. The list of all the Bregman divergences is given in the table below:


Name \(\phi\) \(d_{\phi}\) \(\cal C\)
Euclidean \({\|x\|_2^2}=\sum_{i=1}^dx_i^2\) \(\|x-y\|_2^2\) \(\mathbb{R}^d\)
General Kullback-Leibler \(\sum_{i=1}^d x_i\ln( x_i)\) \(\sum_{i=1}^d( x_i\ln(\frac{ x_i}{y_i})-(x_i-y_i))\) \((0,+\infty)^d\)
Logistic \(\sum_{i=1}^d(x_i\ln( x_i)+(1- x_i)\ln(1- x_i))\) \(\sum_{i=1}^d\Big( x_i\ln(\frac{x_i}{y_i})+(1- x_i)\ln(\frac{1- x_i}{1-y_i})\Big)\) \((0,1)^d\)
Itakura-Saito \(-\sum_{i=1}^d\ln( x_i)\) \(\sum_{i=1}^d\Big(\frac{ x_i}{y_i}-\ln(\frac{ x_i}{y_i})-1\Big)\) \((0,+\infty)^d\)
Exponential \(\sum_{i=1}^de^{x_i}\) \(\sum_{i=1}^d(e^{x_i}-e^{y_i}-e^{y_i}(x_i-y_i))\) \(\mathbb{R}^d\)
Polynomial \(\sum_{i=1}^d|x|^p,p>2\) \(\sum_{k=1}^d(|x_k|^p-|y_k|^p-\text{sign}(y_k)^pp(x_k-y_k)y_k^{p-1})\) \(\mathbb{R}^d\)

2.1 Look-up list of Bregman divergences

The codes below provide a look-up list of all the BDs defined in the table above.

euclidDiv <- function(X., y., deg = NULL){
    res <- sweep(X., 2, y.)
    return(rowSums(res^2))
}
gklDiv <- function(X., y., deg = NULL){
  res <- c("/", "-") %>%
    map(.f = ~ sweep(X., 2, y., FUN = .x))
  return(rowSums(X.*log(res[[1]]) - res[[2]]))
}
logDiv <- function(X., y., deg = NULL){
    res <-  map2(.x = list(X., 1-X.),
                 .y = list(y., 1-y.),
                 .f = ~ sweep(.x, 2, .y, FUN = "/"))
    return(rowSums(X.*log(res[[1]])+(1-X.)*log(res[[2]])))
}
itaDiv <- function(X., y., deg = NULL){
    res <- sweep(X., 2, y., FUN = "/")
    return(rowSums(res-log(res) - 1))
}
expDiv <- function(X., y., deg = NULL){
    exp_y <- exp(y.)
    res <- sweep(1+X., 2, y.) %>%
      sweep(2, exp_y, FUN = "*")
    return(rowSums(exp(X.)-res))
}
polyDiv <- function(X., y., deg = 3){
    S <- map2(.x = list(X., X.^deg),
              .y = list(y., y.^deg),
              .f = ~ sweep(.x, 
                           MARGIN = 2, 
                           STATS = .y,
                           FUN = "-"))
    if(deg %% 2 == 0){
      Tem <- sweep(S[[1]], 2, y.^(deg-1), FUN = "*")
      res <- rowSums(S[[2]] - deg * Tem)
    }
    else{
      Tem <- sweep(S[[1]], 2, sign(y.) * y.^(deg-1), FUN = "*")
      res <- rowSums(S[[2]] - deg * Tem)
    }
    return(res)
}
lookup_div <- list(
  euclidean = euclidDiv,
  gkl = gklDiv,
  logistic = logDiv,
  itakura = itaDiv,
  exponential = expDiv,
  polynomial = polyDiv
)

2.2 Function : BregmanDiv

This function computes Bregman divergence matrix between two sets of data points. Each set of data points should be represented by a matrix, data frame, or tibble object where each row corresponds to each individual data point.

  • Argument:

    • X., C. : data matrices, tibbles and data frames, containing the data points (by row) for which the Bregman divergences between them are to be computed.
    • div : the divergence type to be used. It should be a subset of {"euclidean", "gkl", "logistic", "itakura", "exponential", "polynomial"}.
    • deg : the degree of polynomial BD (if one is used).
  • Value:

    This function returns a tibble object \(D=(d_{i,j})\) where \(d_{i,j}\) is the Bregman divergence between row \(i\) of X. and row \(j\) of C..

BregmanDiv <- function(X., 
                       C., 
                       div = c("euclidean",
                                "gkl",
                                "logistic",
                                "itakura",
                                "exponential",
                                "polynomial"),
                       deg = 3){
  div <- match.arg(div)
  d_c <- dim(C.)
  if(is.null(d_c)){
    C <- matrix(C., nrow = 1, byrow = TRUE)
  } else{
    C <- as.matrix(C.)
  }
  if(is.null(dim(X.))){
    X <- matrix(X., nrow = 1, byrow = TRUE)
  } else{
    X <- as.matrix(X.)
  }
  dis <-  map_dfc(.x = 1:dim(C)[1],
                  .f = ~ tibble('{{.x}}' := lookup_div[[div]](X, C[.x,], deg = deg)))
  return(dis)
}

🧾 Remark.2: Note that “logistic” Bregman divergence can handle only data points with domain \({\cal C}=(0,1)^d\), therefore, it should be used only in suitable cases.


3 Step \(K\): \(K\)-means with Bregman divergences

This section implements \(K\)-means algorithm using Bregman divergences which corresponds to the step \(K\) of KFC procedure.

3.1 Function : findClosestCentroid and newCentroids

These two functions perform the main steps of \(K\)-means algorithm. Function findClosestCentroid assigns any data points to some cluster according to the smallest divergence between the data point and the centroid. It provides a vector of clusters of all the data points. From there, function newCentroids computes new centroids given the cluster labels of all data points.

  • Argument:

    • x. : the data matrices, tibbles and data frames, containing the data points to be assigned to some cluster.
    • centroids : the matrix or data frame of centroids (by row).
    • div : the divergence type to be used.
    • deg : the degree of polynomial BD (if one is used).
  • Value:

    The each of the two functions returns arguments for one another:

    • findClosestCentroid returns a vector of size equals to the number of rows of data matrix x., containing the cluster labels of the data points.
    • newCentroids returns new matrix of centroids.
findClosestCentroid <- function(x., centroids., div, deg = 3){
  dist <- BregmanDiv(x., centroids., div, deg)
  clust <- 1:nrow(x.) %>%
    map_int(.f = ~ which.min(dist[.x,]))
  return(clust)
}
newCentroids <- function(x., clusters.){
  centroids <- unique(clusters.) %>%
    map_dfr(.f = ~ colMeans(x.[clusters. == .x, ]))
  return(centroids)
}

3.2 Function : kmeansBD

This function performs \(K\)-means algorithm with BDs.

  • Argument:

    • train_input : the data matrices, tibbles and data frames, containing the data points.
    • K : the number of clusters.
    • n_start : the number of times to perform the algorithm, and the best one among them is chosen to be the final result. This is done to avoid local optimal solutions. By default, n_start = 5.
    • maxIter : the maximum number of iterations in case the algorithm does not converge. By default, maxIter = 500.
    • deg : the degree of polynomial BD (if one is used).
    • scale_input : a logical value controlling whether to scale the input to be in \((0,1)\) or not. By default, scale_input = FALSE.
    • div : the type of divergence to be used. By default, div = "euclidean" and the usual \(K\)-means algorithm is performed.
    • splits : a real number between \(0\) and \(1\) specifying the proportion of training data to be used to perform \(K\)-means algorithm. The remaining part with be used for the aggregation. By default, splits = 1 and all the input data are used.
    • epsilon : the stopping criterion of the algorithm. By default, epsilon = 1e-10.
    • center_, scale_ : the center and scale to be used to scale the input data. By default, they are NULL.
    • id_shuffle : a logical vector specifying which part of the training data will be selected to perform the algorithm. This is important when we want to perform the algorithm on the same set of data points but with different BDs.
  • Value:

    This function returns a list of the following objects:

    • centroids : the matrix of the centroids obtained by the algorithm.
    • clusters : a vector of cluster labels of the data points.
    • train_data : a list of the following objects:
      • X_train : the training data used for the algorithm.
      • X_remain : the remaining part of the input data used for the aggregation.
      • id_remain : a logical vector specifying the remaining part (X_remain) of the input data.
    • parameters : a list of the following objects:
      • div : divergence used.
      • deg : the degree of polynomial BD (if one is used).
      • center_, scale_ : the center and scale used to scale the input data.
    • running_time: the computational time of the algorithm.
kmeansBD <- function(train_input,
                     K,
                     n_start = 5,
                     maxIter = 500,
                     deg = 3,
                     scale_input = FALSE,
                     div = "euclidean",
                     splits = 1,
                     epsilon = 1e-10,
                     center_ = NULL,
                     scale_ = NULL,
                     id_shuffle = NULL){
  start_time <- Sys.time()
  # Distortion function
  X <- as.matrix(train_input)
  N <- dim(X)
  if(scale_input){
    if(!(is.null(center_) & is.null(scale_))){
      if(length(center_) == 1){
        center_ <- rep(center_, N[2])
      }
      if(length(scale_) == 1){
        scale_ <- rep(scale_, N[2])
      }
    } else{
      min_ <- apply(X, 2, FUN = min)
      c_ <- abs(colMeans(X)/5)
      center_ <- min_ - c_
      scale_ <- apply(X, 2, FUN = max) - center_ + 1
    }
    X <- scale(X, center = center_, scale = scale_)
  }
  if(is.null(id_shuffle)){
    train_id <- rep(TRUE, N[1])
    if(splits < 1){
      train_id[sample(N[1], floor(N[1]*(1-splits)))] <- FALSE
    }
  } else{
    train_id <- id_shuffle
  }
  X_train1 <- X[train_id,]
  X_train2 <- X[!train_id,]
  mu <- as.matrix(colMeans(X_train1))
  distortion <- function(clus){
    cent <- newCentroids(X_train1, clus)
    var_within <- 1:K %>%
      map(.f = ~ BregmanDiv(X_train1[clus == .x,], 
                            cent[.x,], 
                            div, 
                            deg)) %>%
      map(.f = sum) %>%
      Reduce("+", .)
    return(var_within)
  }
  # Kmeans algorithm
  kmeansWithBD <- function(x., k., maxiter., eps.) {
    n. <- nrow(x.)
    # initialization
    init <- sample(n., k.)
    centroids_old <- x.[init,]
    i <- 0
    while(i < maxIter){
      # Assignment step
      clusters <- findClosestCentroid(x., centroids_old, div, deg)
      # Recompute centroids
      centroids_new <- newCentroids(x., clusters)
      if ((sum(is.na(centroids_new)) > 0) |
          (nrow(centroids_new) != k.)) {
        init <- sample(n., k.)
        centroids_old <- x.[init,]
        warning("NA produced -> reinitialize centroids...!")
      }
      else{
        if(sum(abs(centroids_new - centroids_old)) > eps.){
          centroids_old <- centroids_new
        } else{
          break
        }
      }
      i <- i + 1
    }
    return(clusters)
  }
  results <- 1:n_start %>% 
    map_dfc(.f = ~ tibble("{{.x}}" := kmeansWithBD(X_train1, 
                                                   K,
                                                   maxIter, 
                                                   epsilon)))
  opt_id <- 1:n_start %>%
    map_dbl(.f = ~ distortion(results[[.x]])) %>%
    which.min
  cluster <- clusters <- results[[opt_id]]
  j <- 1
  ID <- unique(cluster)
  for (i in ID) {
    clusters[cluster == i] = j
    j =  j + 1
  }
  centroids = newCentroids(X_train1, clusters)
  time_taken <- Sys.time() - start_time
  return(
    list(
      centroids = centroids,
      clusters = clusters,
      train_data = list(X_train = X_train1,
                        X_remain = X_train2,
                        id_remain = !train_id),
      parameters = list(div = div,
                        deg = deg,
                        center_ = center_,
                        scale_ = scale_),
      running_time = time_taken
    )
  )
}

Example.1: We perform \(K\)-means algorithm with "gkl" BD on Abalone dataset.


pacman::p_load(readr)
colname <- c("Type", "LongestShell", "Diameter", "Height", "WholeWeight", "ShuckedWeight", "VisceraWeight", "ShellWeight", "Rings")
df <- readr::read_delim("https://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data", col_names = colname, delim = ",", show_col_types = FALSE)
n <- nrow(df)
train <- logical(n)
train[sample(n,  floor(n*0.8))] <- TRUE
cl <- df[train,2:(ncol(df)-1)] %>%
  kmeansBD(K = 3, div = "gkl", splits = 0.5, scale_input = TRUE)
table(cl$clusters)

  1   2   3 
655 466 550 

4 Step \(F\): Fitting predictive models

This section builds global models by fitting local model on each given cluster of the obtained partition. This corresponds to the step \(F\) of the procedure.

4.1 Function : fitLocalModels

This function fits local models \(({\cal M_{j,k}})_{j,k}\) on all clusters \(k=1,...,K\) of the obtained partition, given by Bregman divergence \({\cal B}_j\), for \(j=1,...,M\).

  • Argument:

    • kmeans_BD : an object obtained from kmeansBD function.
    • train_response : the vector of response variable corresponding to the full input_data given to kmeanBD function.
    • model :a local model type to fit on all the clusters of the given partition. It should be either a model object (works with function predict), or a string element of {“lm”, “tree”, “rf”} which corresponds to linear regression, tree and random forest respectively. By default, model = "lm".
    • formula : the degree of polynomial BD (if one is used).
  • Value:

    This function returns a list of the following objects:

    • local_models : all the local models fitted on all clusters of the given partition.
    • kmeans_BD : the kmeansBD object.
    • data_remain : a list of the following objects:
      • fit : the fitted values of the remaining part of the input data.
      • response : the actual response values corresponding to the remaining input data.
    • running_time : the computational time of the algorithm.
fitLocalModels <- function(kmeans_BD,
                           train_response,
                           model = "lm",
                           formula = NULL){
  start_time <- Sys.time()
  X_train <- kmeans_BD$train_data$X_train
  y_train <- train_response[!(kmeans_BD$train_data$id_remain)]
  X_remain <- kmeans_BD$train_data$X_remain
  y_remain <- NULL
  if(!is.null(X_remain)){
    y_remain <- train_response[kmeans_BD$train_data$id_remain]
  }
  pacman::p_load(tree)
  pacman::p_load(randomForest)
  model_ <- ifelse(model == "tree", tree::tree, model)
  K <- nrow(kmeans_BD$centroids)
  if (is.null(formula)){
    form <- formula(target ~ .)
  }
  else{
    form <- update(formula, target ~ .)
  }
  data_ <- bind_cols(X_train, "target":= y_train)
  fit_lookup <- list(lm = "fitted.values",
                     rf = "predicted")
  if(is.character(model_)){
    model_lookup <- list(lm = lm,
                         rf = randomForest::randomForest)
    mod <- map(.x = 1:K, 
               .f = ~ model_lookup[[model_]](formula = form, 
                                             data = data_[kmeans_BD$clusters == .x, ]))
  } else{
    mod <- map(.x = 1:K, 
               .f = ~ model_(formula = form, 
                             data = data_[kmeans_BD$clusters == .x,]))
  }
  pred0 <- NULL
  if(!is.null(X_remain)){
    pred0 <- vector(mode = "numeric", 
                    length = length(y_remain))
    clus <- findClosestCentroid(x. = X_remain,
                                centroids. = kmeans_BD$centroids,
                                div = kmeans_BD$parameters$div,
                                deg = kmeans_BD$parameters$deg)
    for(i_ in 1:K){
      pred0[clus == i_] <- predict(mod[[i_]],
                                   as.data.frame(X_remain[clus == i_,]))
    }
  }
  time_taken <- Sys.time() - start_time
  return(list(
    local_models = mod,
    kmeans_BD = kmeans_BD,
    data_remain = list(fit = pred0,
                       response = y_remain),
    running_time = time_taken
  ))
}

Example.2: From Example.1 above, multiple linear regression models are built on all the obtained clusters. The mean square error of this model, evaluated on the remaining \(50\%\) of the training data is computed.


fit <- fitLocalModels(train_response = df$Rings[train],
                      kmeans_BD = cl,
                      model = "lm")

mean((fit$data_remain$response- fit$data_remain$fit)^2)
[1] 4.680465

4.2 Function : localPredict

This function allows us to predict any new observations using the candidate model \({\cal M}_j=({\cal M}_{j,k})_{k=1}^M\) corresponding to Bregman divergence \({\cal B}_j\), for some \(j\in J\subset\{1,...,M\}\).

  • Argument:

    • localModels : a local model object obtained from fitLocalModels function.
    • newData : new input data to be predicted using the candidate models given in localModels argument.
  • Value:

    This function returns a predicted vector of the newData.

localPredict <- function(localModels,
                         newData){
  kmean_BD <- localModels$kmeans_BD
  K <- nrow(kmean_BD$centroids)
  newData_ <- newData
  if(!(is.null(kmean_BD$parameters$center_))){
    newData_ <- scale(newData,
                      center = kmean_BD$parameters$center_,
                      scale = kmean_BD$parameters$scale_)
    id0 <- (newData_ <= 0)
    if(sum(id0) > 0){
      min_ <- min(newData_[id0])
      newData_[id0] <- runif(sum(id0), min(1e-3, min_/10), min_)
    }
  }
  clus <- findClosestCentroid(x. = newData_,
                              centroids. = kmean_BD$centroids,
                              div = kmean_BD$parameters$div,
                              deg = kmean_BD$parameters$deg)
  pred0 <- vector(mode = "numeric", length = nrow(newData_))
  for(i_ in 1:K){
    pred0[clus == i_] <- predict(localModels$local_models[[i_]],
                                 as.data.frame(newData_[clus == i_,]))
  }
  pred0 <- as_tibble(pred0)
  names(pred0) <- ifelse(kmean_BD$parameters$div == "polynomial",
                         paste0("polynomial", kmean_BD$parameters$deg),
                         kmean_BD$parameters$div)
  return(pred0)
}

Example.3: The performance of the candidate model corresponding to "gkl" divergence is compared to random forest regression on a \(20\%\) testing data.


y_hat <- localPredict(fit,
                      df[!train, 2:(ncol(df)-1),])
rf <- randomForest(Rings ~ ., data = df[train,2:ncol(df)])
mean((predict(rf, newdata = df[!train,2:ncol(df)])- df$Rings[!train])^2)
[1] 4.345826
mean((y_hat$gkl-df$Rings[!train])^2)
[1] 4.524662

5 Step \(C\): Combining methods

The source codes and information of the aggregation methods are available here . The codes below imports the aggregation methods into Rstudio environment.

pacman::p_load(devtools)
### Kernel based consensual aggregation
source_url("https://raw.githubusercontent.com/hassothea/AggregationMethods/main/KernelAggReg.R")
ℹ SHA-1 hash of file is 813bced0dd2d9aa2431d07b64de08db7a9887bb1
### MixCobra
source_url("https://raw.githubusercontent.com/hassothea/AggregationMethods/main/MixCobraReg.R")
ℹ SHA-1 hash of file is c874cc5e16f484866d07476d002ec77f244989ee

5.1 Function : stepK, stepF and stepC

These functions allow to set values of the parameters in the three steps of the KFC procedure. Each function returns a list of all the parameters given in its arguments.

stepK = function(K,
                 n_start = 5,
                 maxIter = 300,
                 deg = 3,
                 scale_input = FALSE,
                 div = NULL,
                 splits = 0.75,
                 epsilon = 1e-10,
                 center_ = NULL,
                 scale_ = NULL){
  return(list(K = K,
              n_start = n_start,
              maxIter = maxIter,
              deg = deg,
              scale_input = scale_input,
              div = div,
              splits = splits,
              epsilon = epsilon,
              center_ = center_ ,
              scale_ = scale_))
}

stepF = function(formula = NULL, 
                 model = "lm"){
  return(list(formula = formula, 
              model = model))
}

stepC = function(n_cv = 5,
                 method = c("cobra", "mixcobra"),
                 opt_methods = c("grad", "grid"),
                 kernels = "gaussian",
                 scale_features = FALSE){
  return(list(n_cv = n_cv,
              method = method,
              opt_methods = opt_methods,
              kernels = kernels,
              scale_features = scale_features))
}

6 Function: KFCRegressor

This function is the complete implementation of KFC procedure.


🧾 Remark.3: The parallel argument above requires internet connection to load the source codes of \(K\)-means algorithm with BDs from GitHub . It is performed on the maximum number of clusters of your machine, and the speed is at least two times faster than without parallelism, however, it is not so stable depending on your internet connection or machine.

For the aggregation methods in step \(C\):


KFCRegressor = function(train_input,
                  train_response,
                  test_input,
                  test_response = NULL,
                  n_cv = 5,
                  parallel = TRUE,
                  inv_sigma = sqrt(.5),
                  alp = 2,
                  K_step = stepK(splits = .5),
                  F_step = stepF(),
                  C_step = stepC(),
                  setGradParamAgg = setGradParameter(),
                  setGridParamAgg = setGridParameter(),
                  setGradParamMix = setGradParameter_Mix(),
                  setGridParamMix = setGridParameter_Mix(),
                  silent = FALSE){
  start_time <- Sys.time()
  lookup_div_names <- c("euclidean",
                         "gkl",
                         "logistic",
                         "itakura",
                         "polynomial")
  div_ <- K_step$div
  ### K step: Kmeans clustering with BDs
  if (is.null(K_step$div)){
    divergences <- lookup_div_names
    warning("No divergence provided! All of them are used!")
  }
  else{
    divergences <- K_step$div %>% 
      map_chr(.f = ~ match.arg(arg = .x, 
                               choices = lookup_div_names))
  }
  div_list <- divergences %>% 
    map(.f = (\(x) if(x != "polynomial") return(x) else return(rep("polynomial", length(K_step$deg))))) %>%
    unlist
  deg_list <- rep(NA, length(div_))
  deg_list[div_list == "polynomial"] <- K_step$deg
  div_names <- map2_chr(.x = div_list,
                        .y = deg_list,
                        .f = (\(x, y) if(is.na(y)) return(x) else return(paste0(x,y))))
  ### Step K: Kmeans clustering with Bregman divergences
  dm <- dim(train_input)
  id_shuffle <- vector(length = dm[1])
  n_train <- floor(K_step$splits * dm[1])
  id_shuffle[sample(dm[1], n_train)] <- TRUE
  if(parallel){
    numCores <- parallel::detectCores()
    doParallel::registerDoParallel(numCores) # use multicore, set to the number of our cores
    kmean_ <- foreach(i=1:length(div_names)) %dopar% {
      devtools::source_url("https://raw.githubusercontent.com/hassothea/KFC-Procedure/master/kmeanBD.R")
      kmeansBD(train_input = train_input,
               K = K_step$K,
               div = div_list[i],
               n_start = K_step$n_start,
               maxIter = K_step$maxIter,
               deg = deg_list[i],
               scale_input = K_step$scale_input,
               splits = K_step$splits,
               epsilon = K_step$epsilon,
               center_ = K_step$center_,
               scale_ = K_step$scale_,
               id_shuffle = id_shuffle)
    }
    doParallel::stopImplicitCluster()
  } else{
    kmean_ <- map2(.x = div_list,
                   .y = deg_list,
                   .f = ~ kmeansBD(train_input = train_input,
                                   K = K_step$K,
                                   div = .x,
                                   n_start = K_step$n_start,
                                   maxIter = K_step$maxIter,
                                   deg = .y,
                                   scale_input = K_step$scale_input,
                                   splits = K_step$splits,
                                   epsilon = K_step$epsilon,
                                   center_ = K_step$center_,
                                   scale_ = K_step$scale_,
                                   id_shuffle = id_shuffle))
  }
  names(kmean_) <- div_names
  ### F step: Fitting the corresponding model on each observed cluster
  model_ <- div_names %>%
    map(.f = ~ fitLocalModels(kmean_[[.x]],
                              train_response = train_response,
                              model = F_step$model,
                              formula = F_step$formula))
  names(model_) <- div_names
  pred_combine <- model_ %>%
    map_dfc(.f = ~ .x$data_remain$fit)
  y_remain <- train_response[!id_shuffle]
  pred_test <- div_names %>%
    map_dfc(.f = ~ localPredict(model_[[.x]],
                                test_input))
  names(pred_test) <- names(pred_combine) <- div_names
  # C step: Consensual regression aggregation method with kernel-based COBRA
  list_method_agg <- list(mixcobra = function(pred){MixCobraReg(train_input = train_input[!id_shuffle,],
                                                                train_response = y_remain,
                                                                test_input = test_input,
                                                                train_predictions = pred,
                                                                test_predictions = pred_test,
                                                                test_response = test_response,
                                                                scale_input = K_step$scale_input,
                                                                scale_machine = C_step$scale_features,
                                                                n_cv = C_step$n_cv,
                                                                inv_sigma = inv_sigma,
                                                                alp = alp,
                                                                kernels = C_step$kernels,
                                                                optimizeMethod = C_step$opt_methods,
                                                                setGradParam = setGradParamMix,
                                                                setGridParam = setGridParamMix,
                                                                silent = silent)},
                          cobra = function(pred){kernelAggReg(train_design = pred,
                                                              train_response = y_remain,
                                                              test_design = pred_test,
                                                              test_response = test_response,
                                                              scale_input = K_step$scale_input,
                                                              scale_machine = C_step$scale_features,
                                                              build_machine = FALSE,
                                                              machines = NULL,
                                                              n_cv = C_step$n_cv,
                                                              inv_sigma = sqrt(.5),
                                                              alp = 2,
                                                              kernels = C_step$kernels,
                                                              optimizeMethod = C_step$opt_methods,
                                                              setGradParam = setGradParamAgg,
                                                              setGridParam = setGridParamAgg,
                                                              silent = silent)})
  res <- map(.x = C_step$method,
             .f = ~ list_method_agg[[.x]](pred_combine))
  list_agg_methods <- list(cobra = "cob",
                           mixcobra = "mix")
  names(res) <- C_step$method
  ext_fun <- function(L, nam){
    tab <- L$fitted_aggregate
    names(tab) <- paste0(names(tab), "_", nam)
    return(tab)
  }
  pred_fin <- C_step$method %>%
    map_dfc(.f = ~ ext_fun(res[[.x]], list_agg_methods[[.x]]))
  time.taken <- Sys.time() - start_time
  ### To finish
  if(is.null(test_response)){
    return(list(
    predict_final = pred_fin,
    predict_local = pred_test,
    agg_method = res,
    running_time = time.taken
  ))
  } else{
    error <- cbind(pred_test, pred_fin) %>%
      dplyr::mutate(y_test = test_response) %>%
      dplyr::summarise_all(.funs = ~ (. - y_test)) %>%
      dplyr::select(-y_test) %>%
      dplyr::summarise_all(.funs = ~ mean(.^2))
    return(list(
      predict_final = pred_fin,
      predict_local = pred_test,
      agg_method = res,
      mse = error,
      running_time = time.taken
  ))
  }
}

Example.4: A complete KFC procedure is implemented on the same Abalone data, using \(5\) BDs "euclidean", "itakura", "gkl" and "polynomial" (of degree \(3\) and \(6\)). Both aggregation methods are used in the step \(C\). Two kernel functions are used for each aggregation method: "gaussian" (with gradient descent algorithm) and "epanechnikov" (with grid search algorithm).

train1 <- logical(n)
train1[sample(n,  floor(n*0.8))] <- TRUE
kfc1 <- KFCRegressor(train_input = df[train1,2:ncol(df)],
                train_response = df$Rings[train1],
                test_input = df[!train1,2:ncol(df)],
                K_step = stepK(K = 3,
                               scale_input = TRUE,
                               div = c("eucl", "ita", "gkl", "poly"),
                               deg = c(3, 6),
                               splits = .5),
                C_step = stepC(method = c("cobra", "mixcobra"),
                               opt_methods = c("grad", "grid"),
                               kernels = c("gaussian", "gaussian"),
                               scale_features = FALSE),
                setGradParamAgg = setGradParameter(rate = 0.2),
                                                   #coef_lm = 2),
                setGridParamAgg = setGridParameter(min_val = .00001,
                                                   max_val = 10,
                                                   n_val = 100),
                setGradParamMix = setGradParameter_Mix(rate = "linear",
                                                       coef_auto = c(.5,.5)),
                setGridParamMix = setGridParameter_Mix(min_alpha = 1e-10,
                                                       max_alpha = 0.5,
                                                       min_beta = 1e-10,
                                                       max_beta = 1,
                                                       n_alpha = 20,
                                                       n_beta = 20))

* Gradient descent algorithm ...
  Step  |  Parameter    |  Gradient |  Threshold 
 ---------------------------------------------------
   0    |  7.777800     |  -4.40022e-10     |  1e-10 
 ---------------------------------------------------
   1    |  7.977800     |  2.5668e-10   |  0.02571421 

   2    |  7.777800     |  -4.40022e-10     |  0.02506957 

   3    |  7.977800     |  2.5668e-10   |  0.02571421 

   4    |  7.777800     |  -4.40022e-10     |  0.02506957 

   5    |  7.977800     |  2.5668e-10   |  0.02571421 

   6    |  7.777800     |  -4.40022e-10     |  0.02506957 

   7    |  7.977800     |  2.5668e-10   |  0.02571421 

   8    |  7.911133     |  0e+00    |  0.008356523 
-------------------------------------------------------
 Stopped|  7.911133     |  0        |  0
 ~ Observed parameter: 7.911133  in 8 iterations.
* Grid search algorithm... 
 ~ Observed parameter : 8.08081

MixCobra for regression
-----------------------

* Gradient descent algorithm ...
  Step  |  alpha    ;  beta     |  Gradient (alpha ; beta)  |  Threshold 
 --------------------------------------------------------------------------------
   0    |  5.00005  ;  25.05000     |  -4.68285e-04  ;  7.3337e-11      |  1e-10 
 --------------------------------------------------------------------------------
   0    |  5.0000  ;  25.0500   |  -4.6828e-04  ;  7.3337e-11   |  0.004214561 

   1    |  5.5000  ;  24.5500   |  -3.6509e-04  ;  -1.1001e-10  |  0.03327781 

   2    |  6.2797  ;  26.0500   |  -2.2769e-04  ;  2.2001e-10   |  0.07586129 

   3    |  7.0090  ;  21.5500   |  -1.2144e-04  ;  -3.6669e-11  |  0.1617499 

   4    |  7.5277  ;  22.5500   |  -5.7064e-05  ;  3.6669e-10   |  0.05317693 

   5    |  7.8323  ;  10.0500   |  -2.3067e-05  ;  -1.1001e-10  |  0.425719 

   6    |  7.9801  ;  14.5500   |  -7.5142e-06  ;  1.4667e-10   |  0.2599088 

   7    |  8.0363  ;  11.0500   |  -1.7573e-06  ;  -1.1001e-10  |  0.1578404 

   8    |  8.0513  ;  12.5500   |  -2.3244e-07  ;  2.5668e-10   |  0.07937698 

   9    |  8.0535  ;  10.5813   |  -6.3437e-09  ;  -4.4002e-10  |  0.09567286 

   10   |  8.0536  ;  12.4563   |  -7.3337e-11  ;  0e+00    |  0.100622 

   11   |  8.0536  ;  12.4563   |  8.4338e-10  ;  0e+00     |  4.199661e-08 

   12   |  8.0536  ;  12.4563   |  -3.6669e-10  ;  -1.4667e-10  |  5.268665e-07 

   13   |  8.0536  ;  12.6594   |  -3.3002e-10  ;  1.1001e-10   |  0.009903915 

   14   |  8.0536  ;  12.5773   |  -5.1336e-10  ;  3.6669e-11   |  0.003960444 

   15   |  8.0536  ;  12.5627   |  -2.5668e-10  ;  0e+00    |  0.0007101231 

   16   |  8.0536  ;  12.5627   |  7.3337e-11  ;  3.3002e-10    |  5.317427e-08 

   17   |  8.0536  ;  12.4880   |  -3.6669e-11  ;  -1.1001e-10  |  0.003623708 

   18   |  8.0536  ;  12.5012   |  7.3337e-11  ;  3.6669e-10    |  0.000641805 

   19   |  8.0536  ;  12.4780   |  7.3337e-11  ;  -7.3337e-11   |  0.001128374 

   20   |  8.0536  ;  12.4804   |  1.8334e-10  ;  -1.1001e-10   |  0.0001189123 

   21   |  8.0536  ;  12.4823   |  -3.6669e-11  ;  3.3002e-10   |  9.363669e-05 

   22   |  8.0536  ;  12.4763   |  3.6669e-11  ;  3.6669e-11    |  0.0002942408 

   23   |  8.0536  ;  12.4759   |  1.4667e-10  ;  2.2001e-10    |  1.709539e-05 

   24   |  8.0536  ;  12.4738   |  0e+00  ;  0e+00  |  0.0001070309 

   25   |  8.0536  ;  12.4738   |  0e+00  ;  0e+00  |  3.666853e-10 
--------------------------------------------------------------------------------
 Stopped|  8.0536  ;  12.4738   |     0         |  0
 ~ Observed parameter: (alpha, beta) = ( 8.05358 ,  12.47375 ) in 26 itertaions.

* Grid search algorithm...
 ~ Observed parameter: (alpha, beta) = (0.5, 1)

The mean square errors evaluated on \(20\%\)-testing data of the above computation are reported below.

rf1 <- randomForest::randomForest(Rings ~ ., data = df[train1,2:ncol(df)])
kfc1$predict_final %>%
  mutate(rf = predict(rf1, newdata = df[!train1,2:ncol(df)])) %>%
  sweep(MARGIN = 1, STATS = df$Rings[!train1], FUN = "-") %>%
  .^2  %>%
  colMeans
gaussian_grad_cob gaussian_grid_cob gaussian_grad_mix gaussian_grid_mix                rf 
      0.005980861       0.005980861       4.701859057       9.531297947       5.096848245 

We can see that KFC procedure performs really well on this real-life dataset.


📖 Read also KernelAggReg and MixCobraReg methods.


LS0tDQp0aXRsZTogIjxzcGFuIHN0eWxlPSdjb2xvcjogIzFDODFBQTsnPioqS0ZDIHByb2NlZHVyZSBmb3IgcmVncmVzc2lvbiAtPC9zcGFuPiBbSGFzIGV0IGFsLiAoMjAyMSldKGh0dHBzOi8vd3d3LnRhbmRmb25saW5lLmNvbS9lcHJpbnQvWUtHUzhHVEtEQktZRlhFR0ZXU0IvZnVsbD90YXJnZXQ9MTAuMTA4MC8wMDk0OTY1NS4yMDIxLjE4OTE1MzkpKioiDQphdXRob3I6ICI8c3BhbiBzdHlsZT0nY29sb3I6ICNENEE1MUM7Jz4qKipTb3RoZWEgSGFzKioqPC9zcGFuPiINCmRhdGU6ICI1LzIwLzIwMjIiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6DQogICAgY3NzOiBoaWRlT3V0cHV0LmNzcw0KICAgIGluY2x1ZGVzOg0KICAgICAgaW5faGVhZGVyOiBoaWRlT3V0cHV0LnNjcmlwdA0KICAgIGRmX3ByaW50OiBwYWdlZA0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6ICcyJw0KICAgIHRvY2RlcHRoOiAyDQogIGh0bWxfbm90ZWJvb2s6DQogICAgY3NzOiBoaWRlT3V0cHV0LmNzcw0KICAgIGluY2x1ZGVzOg0KICAgICAgaW5faGVhZGVyOiBoaWRlT3V0cHV0LnNjcmlwdA0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6IDINCiAgICB0b2NkZXB0aDogMg0KICBwZGZfZG9jdW1lbnQ6DQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6ICcyJw0KLS0tDQoNCjxzdHlsZT4NCiAgLmJ0biB7DQogICAgYm9yZGVyLXdpZHRoOiAwIDBweCAwcHggMHB4Ow0KICAgIGZvbnQtd2VpZ2h0OiBub3JtYWw7DQogICAgdGV4dC10cmFuc2Zvcm06IDsNCiAgfQ0KLmJ0bi1kZWZhdWx0IHsNCiAgY29sb3I6ICMyZWNjNzE7DQogICAgYmFja2dyb3VuZC1jb2xvcjogI2ZmZmZmZjsNCiAgICBib3JkZXItY29sb3I6ICNmZmZmZmY7DQp9DQo8L3N0eWxlPg0KDQo8IS0tIENvbG9ycw0KYmx1ZSA6ICMxRkFBRTMNCnllbGxvdyA6ICNGMEFFMTQNCmdyZWVuIDogIzU0RDMxOSANCnJlZCA6ICNFNjE4MEENCi0tPg0KDQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KIyBDaGVjayBpZiBwYWNrYWdlICJmb250YXdlc29tZSIgaXMgYWxyZWFkeSBpbnN0YWxsZWQgDQoNCmxvb2t1cF9wYWNrYWdlcyA8LSBpbnN0YWxsZWQucGFja2FnZXMoKVssMV0NCmlmKCEoImZvbnRhd2Vzb21lIiAlaW4lIGxvb2t1cF9wYWNrYWdlcykpDQogIGluc3RhbGwucGFja2FnZXMoImZvbnRhd2Vzb21lIikNCmBgYA0KDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogIzFGQUFFMzsiPiYjMTI4MjcwOzx1PiBIb3cgdG8gZG93bmxvYWQgJiBydW4gdGhlIGNvZGVzPzwvdT48L3NwYW4+ey19DQo9PT0NCg0KQWxsIHRoZSBzb3VyY2UgY29kZXMgb2YgdGhlIGFnZ3JlZ2F0aW9uIG1ldGhvZHMgYXJlIGF2YWlsYWJsZSBbaGVyZSA8c3BhbiBzdHlsZT0iY29sb3I6ICMwOTdCQzEiPiBgciBmb250YXdlc29tZTo6ZmEoImdpdGh1YiIpYDwvc3Bhbj5dKGh0dHBzOi8vZ2l0aHViLmNvbS9oYXNzb3RoZWEvQWdncmVnYXRpb25NZXRob2RzKS4gVG8gcnVuIHRoZSBjb2RlcywgeW91IGNhbiA8c3BhbiBzdHlsZT0iY29sb3I6ICMwOTdCQzEiPmBjbG9uZWA8L3NwYW4+IHRoZSByZXBvc2l0b3J5IGRpcmVjdGx5IG9yIHNpbXBseSBsb2FkIHRoZSA8c3BhbiBzdHlsZT0iY29sb3I6ICMwOTdCQzEiPmBSIHNjcmlwdGA8L3NwYW4+IHNvdXJjZSBmaWxlIGZyb20gdGhlIHJlcG9zaXRvcnkgdXNpbmcgW2RldnRvb2xzXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvZGV2dG9vbHMvaW5kZXguaHRtbCkgcGFja2FnZSBpbiA8c3BhbiBzdHlsZT0iY29sb3I6ICMwMjg3RDg7Ij4gKipSc3R1ZGlvKiogPC9zcGFuPiBhcyBmb2xsb3c6DQoNCjEuIEluc3RhbGwgW2RldnRvb2xzXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvZGV2dG9vbHMvaW5kZXguaHRtbCkgcGFja2FnZSB1c2luZyBjb21tYW5kOiANCg0KICAgIGBpbnN0YWxsLnBhY2thZ2VzKCJkZXZ0b29scyIpYA0KDQoyLiBMb2FkaW5nIHRoZSBzb3VyY2UgY29kZXMgZnJvbSA8c3BhbiBzdHlsZT0iY29sb3I6ICMwOTdCQzEiPkdpdEh1YiBgciBmb250YXdlc29tZTo6ZmEoImdpdGh1YiIpYDwvc3Bhbj4gcmVwb3NpdG9yeSB1c2luZyBgc291cmNlX3VybGAgZnVuY3Rpb24gYnk6IA0KDQogICAgYGRldnRvb2xzOjpzb3VyY2VfdXJsKCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vaGFzc290aGVhL0tGQy1Qcm9jZWR1cmUvbWFzdGVyL0tGQ1JlZy5SIilgDQoNCi0tLQ0KDQo+ICoqJiM5OTk4OyBOb3RlKio6IEFsbCBjb2RlcyBjb250YWluZWQgaW4gdGhpcyBgUm1hcmtkb3duYCBhcmUgYnVpbHQgd2l0aCByZWNlbnQgdmVyc2lvbiBvZiA8c3BhbiBzdHlsZT0iY29sb3I6ICMwOTdCQzEiPmByIGZvbnRhd2Vzb21lOjpmYSgici1wcm9qZWN0IilgPC9zcGFuPiAodmVyc2lvbiAkPiQgNC4xLCBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL2Jpbi93aW5kb3dzL2Jhc2UvKSkgYW5kIDxzcGFuIHN0eWxlPSJjb2xvcjogIzAyODdEODsiPiAqKlJzdHVkaW8qKiA8L3NwYW4+ICh2ZXJzaW9uID4gYDIwMjIuMDIuMis0ODVgLCBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vd3d3LnJzdHVkaW8uY29tL3Byb2R1Y3RzL3JzdHVkaW8vZG93bmxvYWQvI2Rvd25sb2FkKSkuIE5vdGUgYWxzbyB0aGF0IHRoZSBjb2RlIGNodWNrcyBhcmUgPHNwYW4gc3R5bGU9ImNvbG9yOiAjRTYxODBBOyI+aGlkZGVuPC9zcGFuPiBieSBkZWZhdWx0Lg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQiPiAqKlRvIHNlZSB0aGUgY29kZXMsIHlvdSBjYW46KiogPC9zcGFuPg0KDQotIGNsaWNrIG9uIHRoZSB0b3AtcmlnaHQgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNTREMzE5IDsiPkNvZGU8L3NwYW4+IGJ1dHRvbiBvZiB0aGUgcGFnZSwgdGhlbiBjaG9vc2UgKipTaG93IEFsbCBDb2RlKiogdG8gc2hvdyBhbGwgdGhlIGNvZGVzLCBvciANCi0gc2ltcGx5IGNsaWNrIG9uIHRoZSByaWdodC1jb3JuZXIgPHNwYW4gc3R5bGU9ImNvbG9yOiAjNTREMzE5IDsiPkNvZGU8L3NwYW4+IGJ1dHRvbiBhdCBlYWNoIHNlY3Rpb24gdG8gc2hvdyB0aGUgY29kZXMgb2YgdGhhdCBzcGVjaWZpYyBzZWN0aW9uLg0KDQotLS0NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+PHU+S0ZDIHByb2NlZHVyZSAmIGltcG9ydGFudCBwYWNrYWdlcyA8L3U+PC9zcGFuPg0KPT09DQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogI0YwQUUxNDsiPjx1PktGQyBwcm9jZWR1cmU8L3U+PC9zcGFuPg0KLS0tDQoNCktGQyBwcm9jZWR1cmUgaXMgYSB0aHJlZS1zdGVwIG1ldGhvZG9sb2d5IHdoaWNoIHB1dHMgdG9nZXRoZXIgY2x1c3RlcmluZyBhbmQgY29uc2Vuc3VhbCBhZ2dyZWdhdGlvbiBtZXRob2RzIGZvciBidWlsZGluZyBwcmVkaWN0aW9ucyBpbiBzdXBlcnZpc2VkIGxlYXJuaW5nIHByb2JsZW1zLiBUaGUgcHJvY2VkdXJlIGlzIGluc3BpcmVkIGJ5IG1hbnkgcmVhbC1saWZlIHByZWRpY3Rpb24gcHJvYmxlbXMgd2hlbiB0aGUgaW4gVGhlIHRocmVlIHN0ZXBzIG9mIHRoZSBwcm9jZWR1cmUgYXJlOg0KDQotICoqU3RlcCAqSyogKio6ICRLJC1tZWFucyBjbHVzdGVyaW5nIGFsZ29yaXRobSBpcyBpbXBsZW1lbnRlZCBvbiB0aGUgaW5wdXQgZGF0YSB1c2luZyBzZXZlcmFsIG9wdGlvbnMgb2YgQnJlZ21hbiBkaXZlcmdlbmNlcyAkKHtcY2FsIEJ9X2opX3tqPTF9Xk0kICgkTSQgaXMgdGhlIG51bWJlciBvZiB0b3RhbCBkaXZlcmdlbmNlcyB1c2VkKSwgdGhlcmVmb3JlLCB0aGUgaW5wdXQgZGF0YSBpcyBwYXJ0aXRpb25lZCBpbnRvIG1hbnkgZGlmZmVyZW50IHN0cnVjdHVyZXMsIGFjY29yZGluZyB0byB0aGUgcHJvcGVydHkgb2YgZWFjaCBCcmVnbWFuIGRpdmVyZ2VuY2UuDQotICoqU3RlcCAqRiogKio6IEZvciBhIHBhcnRpdGlvbiBzdHJ1Y3R1cmUgZ2l2ZW4gYnkgYSBkaXZlcmdlbmNlICR7XGNhbCBCfV9qJCwgd2UgZml0IHNpbXBsZSBtb2RlbHMgKGxpbmVhciwgZm9yIGV4YW1wbGUpIG9uIGFsbCB0aGUgY2x1c3RlcnMgb2YgdGhlIG9idGFpbmVkIHBhcnRpdGlvbi4gVGhlbiwgdGhlIGNvbGxlY3Rpb24gJHtcY2FsIE19X2o9XHt7XGNhbCBNfV97aixrfVx9X3trPTF9XkskIG9mIHRoZXNlIGxvY2FsIG1vZGVscyBpcyBjYWxsZWQgKmNhbmRpZGF0ZSogbW9kZWwsIGNvcnJlc3BvbmRpbmcgdG8gdGhlIEJyZWdtYW4gZGl2ZXJnZW5jZSAke1xjYWwgQn1faiQuIEF0IHRoZSBlbmQgb2YgdGhpcyBzdGVwLCBzZXZlcmFsIGNhbmRpZGF0ZSBtb2RlbHMgYXJlIGNvbnN0cnVjdGVkLiANCi0gKipTdGVwICpDKiAqKjogVGhpcyBzdGVwIGFnZ3JlZ2F0ZXMgdGhlIG9idGFpbmVkIGNhbmRpZGF0ZSBtb2RlbHMgdXNpbmcgY29uc2Vuc3VhbCBhZ2dyZWdhdGlvbiBtZXRob2RzIHN0dWRpZWQgaW4gW0hhcyAoMjAyMSldKGh0dHBzOi8vaGFsLmFyY2hpdmVzLW91dmVydGVzLmZyL2hhbC0wMjg4NDMzM3Y1KSBvciBbRmlzY2hlciBhbmQgTW91Z2VvdCAoMjAxOSldKGh0dHBzOi8vd3d3LnNjaWVuY2VkaXJlY3QuY29tL3NjaWVuY2UvYXJ0aWNsZS9waWkvUzAzNzgzNzU4MTgzMDIzNDkpLg0KDQotLS0NCg0KIVtUaGUgZmlndXJlIGFib3ZlIHJlcHJlc2VudHMgdGhlIHN1bW1hcnkgb2YgS0ZDIHByb2NlZHVyZV0oLi9rZmMucG5nKQ0KDQotLS0NCg0KPiDwn6e+ICoqUmVtYXJrLjEqKjogDQpUaGUgcHJlZGljdGlvbiBvZiBhbnkgb2JzZXJ2YXRpb24gJHgkIGdpdmVuIGJ5IGEgY2FuZGlkYXRlIG1vZGVsICR7XGNhbCBNfV9qJCBpcyBkb25lIGluIHR3byBzaW1wbGUgc3RlcHM6DQoNCjEuICR4JCBpcyBjbGFzc2lmaWVkIGludG8gb25lIG9mIHRoZSBvYnRhaW5lZCBjbHVzdGVycyB1c2luZyB0aGUgY29ycmVzcG9uZGluZyBkaXZlcmdlbmNlICR7XGNhbCBCfV9qJCwgaS5lLiwNCiAgJCR4XGlue1xjYWwgQ31fe2teKn0gXExlZnRyaWdodGFycm93IHtcY2FsIEJ9X2ooY197a14qfSx4KT1caW5mX3sxXGxlcSBrXGxlcSBLfXtcY2FsIEJ9X2ooY19rLHgpJCQNCndoZXJlICRce2NfMSwuLi4sY19LXH1fe2s9MX1eSyQgYXJlIHRoZSBjZW50cm9pZHMgb2YgdGhlIGNvcnJlc3BvbmRpbmcgY2x1c3RlcnMgJFx7Q18xLC4uLixDX0tcfSQuDQoNCjIuIFRoZSBwcmVkaWN0aW9uIG9mICR4JCBpcyBnaXZlbiBieSB0aGUgY29ycmVzcG9uZGluZyBsb2NhbCBtb2RlbCAke1xjYWwgTX1fe2osa14qfSQgZGVmaW5lZCBvbiBjbHVzdGVyICRrXiokLCBpLmUuLCAke1xjYWwgTX1faih4KT17XGNhbCBNfV97aixrXip9KHgpJC4NCg0KLS0tDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogI0YwQUUxNDsiPjx1PkltcG9ydGFudCBwYWNrYWdlczwvdT48L3NwYW4+DQotLS0NCg0KV2UgcHJlcGFyZSBhbGwgdGhlIG5lY2Vzc2FyeSB0b29scyBmb3IgdGhpcyBgUm1hcmtkb3duYC4gVGhlIGBwYWNtYW5gIHBhY2thZ2UgYWxsb3dzIHVzIHRvIGxvYWQgKGlmIGV4aXN0cykgb3IgaW5zdGFsbCAoaWYgZG9lcyBub3QgZXhpc3QpIGFueSBhdmFpbGFibGUgcGFja2FnZXMgZnJvbSBbVGhlIENvbXByZWhlbnNpdmUgUiBBcmNoaXZlIE5ldHdvcmsgKENSQU4pXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy8pIG9mIDxzcGFuIHN0eWxlPSJjb2xvcjogIzA5N0JDMSI+YHIgZm9udGF3ZXNvbWU6OmZhKCJyLXByb2plY3QiKWA8L3NwYW4+LiANCg0KDQpgYGB7cn0NCiMgQ2hlY2sgaWYgcGFja2FnZSAicGFjbWFuIiBpcyBhbHJlYWR5IGluc3RhbGxlZCANCg0KbG9va3VwX3BhY2thZ2VzIDwtIGluc3RhbGxlZC5wYWNrYWdlcygpWywxXQ0KaWYoISgicGFjbWFuIiAlaW4lIGxvb2t1cF9wYWNrYWdlcykpDQogIGluc3RhbGwucGFja2FnZXMoInBhY21hbiIpDQoNCg0KIyBUbyBiZSBpbnN0YWxsZWQgb3IgbG9hZGVkDQpwYWNtYW46OnBfbG9hZChtYWdyaXR0cikNCnBhY21hbjo6cF9sb2FkKHRpZHl2ZXJzZSkNCg0KIyMgcGFja2FnZSBmb3IgImdlbmVyYXRlTWFjaGluZXMiDQpwYWNtYW46OnBfbG9hZCh0cmVlKQ0KcGFjbWFuOjpwX2xvYWQoZ2xtbmV0KQ0KcGFjbWFuOjpwX2xvYWQocmFuZG9tRm9yZXN0KQ0KcGFjbWFuOjpwX2xvYWQoRk5OKQ0KcGFjbWFuOjpwX2xvYWQoeGdib29zdCkNCnBhY21hbjo6cF9sb2FkKGtlcmFzKQ0KcGFjbWFuOjpwX2xvYWQocHJhY21hKQ0KcGFjbWFuOjpwX2xvYWQobGF0ZXgyZXhwKQ0KcGFjbWFuOjpwX2xvYWQocGxvdGx5KQ0KcGFjbWFuOjpwX2xvYWQocGFyYWxsZWwpDQpwYWNtYW46OnBfbG9hZChmb3JlYWNoKQ0KcGFjbWFuOjpwX2xvYWQoZG9QYXJhbGxlbCkNCnJtKGxvb2t1cF9wYWNrYWdlcykNCmBgYA0KDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogIzFGQUFFMzsiPjx1PkJyZWdtYW4gZGl2ZXJnZW5jZXMgKEJEKTwvdT48L3NwYW4+DQo9PT0NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+KipEZWZpbml0aW9uKio8L3NwYW4+IExldCAkXHBoaTpcbWF0aGNhbHtDfVxyaWdodGFycm93XG1hdGhiYntSfSQgYmUgYSBzdHJpY3RseSBjb252ZXggYW5kIGNvbnRpbnVvdXNseSBkaWZmZXJlbnRpYWJsZSBmdW5jdGlvbiBkZWZpbmVkIG9uIGEgbWVhc3VyYWJsZSBjb252ZXggc3Vic2V0ICRcbWF0aGNhbHtDfVxzdWJzZXRcbWF0aGJie1J9XmQkLiBMZXQgJGludChcbWF0aGNhbHtDfSkkIGRlbm90ZSBpdHMgcmVsYXRpdmUgaW50ZXJpb3IuIEEgQnJlZ21hbiBkaXZlcmdlbmNlIGluZGV4ZWQgYnkgJFxwaGkkIGlzIGEgZGlzc2ltaWxhcml0eSBtZWFzdXJlICRkX3tccGhpfTpcbWF0aGNhbHtDfVx0aW1lcyBpbnQoXG1hdGhjYWx7Q30pXHJpZ2h0YXJyb3dcbWF0aGJie1J9JCBkZWZpbmVkIGZvciBhbnkgcGFpciAkKHgseSlcaW4gXG1hdGhjYWx7Q31cdGltZXMgaW50KFxtYXRoY2Fse0N9KSQgYnksDQpcYmVnaW57ZXF1YXRpb259DQpcbGFiZWx7ZXE6MS4xMH0NCmRfe1xwaGl9KHgseSk9XHBoaSh4KS1ccGhpKHkpLVxsYW5nbGUgeC15LFxuYWJsYVxwaGkoeSlccmFuZ2xlIA0KXGVuZHtlcXVhdGlvbn0NCndoZXJlICRcbmFibGFccGhpKHkpJCBkZW5vdGVzIHRoZSBncmFkaWVudCBvZiAkXHBoaSQgY29tcHV0ZWQgYXQgYSBwb2ludCAkeVxpbiBpbnQoXG1hdGhjYWx7Q30pJC4gQSBCcmVnbWFuIGRpdmVyZ2VuY2UgaXMgbm90IG5lY2Vzc2FyaWx5IGEgbWV0cmljIGFzIGl0IG1heSBub3QgYmUgc3ltbWV0cmljIGFuZCB0aGUgdHJpYW5ndWxhciBpbmVxdWFsaXR5IG1pZ2h0IG5vdCBiZSBzYXRpc2ZpZWQuDQoNClRoaXMgc2VjdGlvbiBkZWZpbmVzIGFsbCB0aGUgQnJlZ21hbiBkaXZlcmdlbmNlcyB1c2VkLiBUaGUgbGlzdCBvZiBhbGwgdGhlIEJyZWdtYW4gZGl2ZXJnZW5jZXMgaXMgZ2l2ZW4gaW4gdGhlIHRhYmxlIGJlbG93Og0KDQotLS0tDQoNCk5hbWUgICAgICAgICAgICAgICAgICAgICAgICAkXHBoaSQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJGRfe1xwaGl9JCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAkXGNhbCBDJA0KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAtLS0tLS0tLS0tLQ0KRXVjbGlkZWFuICAgICAgICAgICAgICAgICAke1x8eFx8XzJeMn09XHN1bV97aT0xfV5keF9pXjIkICAgICAgICAgICAgICAgICAgICRcfHgteVx8XzJeMiQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAkXG1hdGhiYntSfV5kJA0KR2VuZXJhbCBLdWxsYmFjay1MZWlibGVyICAkXHN1bV97aT0xfV5kIHhfaVxsbiggeF9pKSQgICAgICAgICAgICAgICAgICAgICAgICRcc3VtX3tpPTF9XmQoIHhfaVxsbihcZnJhY3sgeF9pfXt5X2l9KS0oeF9pLXlfaSkpJCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAkKDAsK1xpbmZ0eSleZCQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgDQpMb2dpc3RpYyAgICAgICAgICAgICAgICAgICRcc3VtX3tpPTF9XmQoeF9pXGxuKCB4X2kpKygxLSB4X2kpXGxuKDEtIHhfaSkpJCAgJFxzdW1fe2k9MX1eZFxCaWcoIHhfaVxsbihcZnJhY3t4X2l9e3lfaX0pKygxLSB4X2kpXGxuKFxmcmFjezEtIHhfaX17MS15X2l9KVxCaWcpJCAgICQoMCwxKV5kJA0KSXRha3VyYS1TYWl0byAgICAgICAgICAgICAkLVxzdW1fe2k9MX1eZFxsbiggeF9pKSQgICAgICAgICAgICAgICAgICAgICAgICAgICRcc3VtX3tpPTF9XmRcQmlnKFxmcmFjeyB4X2l9e3lfaX0tXGxuKFxmcmFjeyB4X2l9e3lfaX0pLTFcQmlnKSQgICAgICAgICAgICAgICAgICAgICQoMCwrXGluZnR5KV5kJA0KRXhwb25lbnRpYWwgICAgICAgICAgICAgICAkXHN1bV97aT0xfV5kZV57eF9pfSQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICRcc3VtX3tpPTF9XmQoZV57eF9pfS1lXnt5X2l9LWVee3lfaX0oeF9pLXlfaSkpJCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICRcbWF0aGJie1J9XmQkDQpQb2x5bm9taWFsICAgICAgICAgICAgICAgICRcc3VtX3tpPTF9XmR8eHxecCxwPjIkICAgICAgICAgICAgICAgICAgICAgICAgICAgJFxzdW1fe2s9MX1eZCh8eF9rfF5wLXx5X2t8XnAtXHRleHR7c2lnbn0oeV9rKV5wcCh4X2steV9rKXlfa157cC0xfSkkICAgICAgICAgICAgICRcbWF0aGJie1J9XmQkDQoNCi0tLS0NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+PHU+TG9vay11cCBsaXN0IG9mIEJyZWdtYW4gZGl2ZXJnZW5jZXM8L3U+PC9zcGFuPg0KLS0tLQ0KDQpUaGUgY29kZXMgYmVsb3cgcHJvdmlkZSBhIGxvb2stdXAgbGlzdCBvZiBhbGwgdGhlIEJEcyBkZWZpbmVkIGluIHRoZSB0YWJsZSBhYm92ZS4NCg0KYGBge3J9DQpldWNsaWREaXYgPC0gZnVuY3Rpb24oWC4sIHkuLCBkZWcgPSBOVUxMKXsNCiAgICByZXMgPC0gc3dlZXAoWC4sIDIsIHkuKQ0KICAgIHJldHVybihyb3dTdW1zKHJlc14yKSkNCn0NCmdrbERpdiA8LSBmdW5jdGlvbihYLiwgeS4sIGRlZyA9IE5VTEwpew0KICByZXMgPC0gYygiLyIsICItIikgJT4lDQogICAgbWFwKC5mID0gfiBzd2VlcChYLiwgMiwgeS4sIEZVTiA9IC54KSkNCiAgcmV0dXJuKHJvd1N1bXMoWC4qbG9nKHJlc1tbMV1dKSAtIHJlc1tbMl1dKSkNCn0NCmxvZ0RpdiA8LSBmdW5jdGlvbihYLiwgeS4sIGRlZyA9IE5VTEwpew0KICAgIHJlcyA8LSAgbWFwMigueCA9IGxpc3QoWC4sIDEtWC4pLA0KICAgICAgICAgICAgICAgICAueSA9IGxpc3QoeS4sIDEteS4pLA0KICAgICAgICAgICAgICAgICAuZiA9IH4gc3dlZXAoLngsIDIsIC55LCBGVU4gPSAiLyIpKQ0KICAgIHJldHVybihyb3dTdW1zKFguKmxvZyhyZXNbWzFdXSkrKDEtWC4pKmxvZyhyZXNbWzJdXSkpKQ0KfQ0KaXRhRGl2IDwtIGZ1bmN0aW9uKFguLCB5LiwgZGVnID0gTlVMTCl7DQogICAgcmVzIDwtIHN3ZWVwKFguLCAyLCB5LiwgRlVOID0gIi8iKQ0KICAgIHJldHVybihyb3dTdW1zKHJlcy1sb2cocmVzKSAtIDEpKQ0KfQ0KZXhwRGl2IDwtIGZ1bmN0aW9uKFguLCB5LiwgZGVnID0gTlVMTCl7DQogICAgZXhwX3kgPC0gZXhwKHkuKQ0KICAgIHJlcyA8LSBzd2VlcCgxK1guLCAyLCB5LikgJT4lDQogICAgICBzd2VlcCgyLCBleHBfeSwgRlVOID0gIioiKQ0KICAgIHJldHVybihyb3dTdW1zKGV4cChYLiktcmVzKSkNCn0NCnBvbHlEaXYgPC0gZnVuY3Rpb24oWC4sIHkuLCBkZWcgPSAzKXsNCiAgICBTIDwtIG1hcDIoLnggPSBsaXN0KFguLCBYLl5kZWcpLA0KICAgICAgICAgICAgICAueSA9IGxpc3QoeS4sIHkuXmRlZyksDQogICAgICAgICAgICAgIC5mID0gfiBzd2VlcCgueCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICBNQVJHSU4gPSAyLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFNUQVRTID0gLnksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBGVU4gPSAiLSIpKQ0KICAgIGlmKGRlZyAlJSAyID09IDApew0KICAgICAgVGVtIDwtIHN3ZWVwKFNbWzFdXSwgMiwgeS5eKGRlZy0xKSwgRlVOID0gIioiKQ0KICAgICAgcmVzIDwtIHJvd1N1bXMoU1tbMl1dIC0gZGVnICogVGVtKQ0KICAgIH0NCiAgICBlbHNlew0KICAgICAgVGVtIDwtIHN3ZWVwKFNbWzFdXSwgMiwgc2lnbih5LikgKiB5Ll4oZGVnLTEpLCBGVU4gPSAiKiIpDQogICAgICByZXMgPC0gcm93U3VtcyhTW1syXV0gLSBkZWcgKiBUZW0pDQogICAgfQ0KICAgIHJldHVybihyZXMpDQp9DQpsb29rdXBfZGl2IDwtIGxpc3QoDQogIGV1Y2xpZGVhbiA9IGV1Y2xpZERpdiwNCiAgZ2tsID0gZ2tsRGl2LA0KICBsb2dpc3RpYyA9IGxvZ0RpdiwNCiAgaXRha3VyYSA9IGl0YURpdiwNCiAgZXhwb25lbnRpYWwgPSBleHBEaXYsDQogIHBvbHlub21pYWwgPSBwb2x5RGl2DQopDQpgYGANCg0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQ7Ij48dT5GdW5jdGlvbjwvdT48L3NwYW4+IDogYEJyZWdtYW5EaXZgDQotLS0tDQoNClRoaXMgZnVuY3Rpb24gY29tcHV0ZXMgQnJlZ21hbiBkaXZlcmdlbmNlIG1hdHJpeCBiZXR3ZWVuIHR3byBzZXRzIG9mIGRhdGEgcG9pbnRzLiBFYWNoIHNldCBvZiBkYXRhIHBvaW50cyBzaG91bGQgYmUgcmVwcmVzZW50ZWQgYnkgYSBtYXRyaXgsIGRhdGEgZnJhbWUsIG9yIHRpYmJsZSBvYmplY3Qgd2hlcmUgZWFjaCByb3cgY29ycmVzcG9uZHMgdG8gZWFjaCBpbmRpdmlkdWFsIGRhdGEgcG9pbnQuDQoNCi0gKipBcmd1bWVudCoqOg0KDQogICAgLSBgWC5gLCBgQy5gIDogZGF0YSBtYXRyaWNlcywgdGliYmxlcyBhbmQgZGF0YSBmcmFtZXMsIGNvbnRhaW5pbmcgdGhlIGRhdGEgcG9pbnRzIChieSByb3cpIGZvciB3aGljaCB0aGUgQnJlZ21hbiBkaXZlcmdlbmNlcyBiZXR3ZWVuIHRoZW0gYXJlIHRvIGJlIGNvbXB1dGVkLg0KICAgIC0gYGRpdmAgOiB0aGUgZGl2ZXJnZW5jZSB0eXBlIHRvIGJlIHVzZWQuIEl0IHNob3VsZCBiZSBhIHN1YnNldCBvZiBgeyJldWNsaWRlYW4iLCAiZ2tsIiwgImxvZ2lzdGljIiwgIml0YWt1cmEiLCAiZXhwb25lbnRpYWwiLCAicG9seW5vbWlhbCJ9YC4NCiAgICAtIGBkZWdgIDogdGhlIGRlZ3JlZSBvZiBwb2x5bm9taWFsIEJEIChpZiBvbmUgaXMgdXNlZCkuDQotICoqVmFsdWUqKjogDQogICAgDQogICAgVGhpcyBmdW5jdGlvbiByZXR1cm5zIGEgKnRpYmJsZSogb2JqZWN0ICREPShkX3tpLGp9KSQgd2hlcmUgJGRfe2ksan0kIGlzIHRoZSBCcmVnbWFuIGRpdmVyZ2VuY2UgYmV0d2VlbiByb3cgJGkkIG9mIGBYLmAgYW5kIHJvdyAkaiQgb2YgYEMuYC4NCg0KDQpgYGB7cn0NCkJyZWdtYW5EaXYgPC0gZnVuY3Rpb24oWC4sIA0KICAgICAgICAgICAgICAgICAgICAgICBDLiwgDQogICAgICAgICAgICAgICAgICAgICAgIGRpdiA9IGMoImV1Y2xpZGVhbiIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJna2wiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibG9naXN0aWMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiaXRha3VyYSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJleHBvbmVudGlhbCIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwb2x5bm9taWFsIiksDQogICAgICAgICAgICAgICAgICAgICAgIGRlZyA9IDMpew0KICBkaXYgPC0gbWF0Y2guYXJnKGRpdikNCiAgZF9jIDwtIGRpbShDLikNCiAgaWYoaXMubnVsbChkX2MpKXsNCiAgICBDIDwtIG1hdHJpeChDLiwgbnJvdyA9IDEsIGJ5cm93ID0gVFJVRSkNCiAgfSBlbHNlew0KICAgIEMgPC0gYXMubWF0cml4KEMuKQ0KICB9DQogIGlmKGlzLm51bGwoZGltKFguKSkpew0KICAgIFggPC0gbWF0cml4KFguLCBucm93ID0gMSwgYnlyb3cgPSBUUlVFKQ0KICB9IGVsc2V7DQogICAgWCA8LSBhcy5tYXRyaXgoWC4pDQogIH0NCiAgZGlzIDwtICBtYXBfZGZjKC54ID0gMTpkaW0oQylbMV0sDQogICAgICAgICAgICAgICAgICAuZiA9IH4gdGliYmxlKCd7ey54fX0nIDo9IGxvb2t1cF9kaXZbW2Rpdl1dKFgsIENbLngsXSwgZGVnID0gZGVnKSkpDQogIHJldHVybihkaXMpDQp9DQpgYGANCg0KDQotLS0NCg0KPiDwn6e+ICoqUmVtYXJrLjIqKjogDQpOb3RlIHRoYXQgImxvZ2lzdGljIiBCcmVnbWFuIGRpdmVyZ2VuY2UgY2FuIGhhbmRsZSBvbmx5IGRhdGEgcG9pbnRzIHdpdGggZG9tYWluICR7XGNhbCBDfT0oMCwxKV5kJCwgdGhlcmVmb3JlLCBpdCBzaG91bGQgYmUgdXNlZCBvbmx5IGluIHN1aXRhYmxlIGNhc2VzLg0KDQotLS0NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+PHU+U3RlcCAkSyQ6ICRLJC1tZWFucyB3aXRoIEJyZWdtYW4gZGl2ZXJnZW5jZXM8L3U+PC9zcGFuPg0KPT09DQoNClRoaXMgc2VjdGlvbiBpbXBsZW1lbnRzICRLJC1tZWFucyBhbGdvcml0aG0gdXNpbmcgQnJlZ21hbiBkaXZlcmdlbmNlcyB3aGljaCBjb3JyZXNwb25kcyB0byB0aGUgc3RlcCAkSyQgb2YgS0ZDIHByb2NlZHVyZS4NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+PHU+RnVuY3Rpb248L3U+PC9zcGFuPiA6IGBmaW5kQ2xvc2VzdENlbnRyb2lkYCBhbmQgYG5ld0NlbnRyb2lkc2ANCi0tLS0NCg0KVGhlc2UgdHdvIGZ1bmN0aW9ucyBwZXJmb3JtIHRoZSBtYWluIHN0ZXBzIG9mICRLJC1tZWFucyBhbGdvcml0aG0uIEZ1bmN0aW9uIGBmaW5kQ2xvc2VzdENlbnRyb2lkYCBhc3NpZ25zIGFueSBkYXRhIHBvaW50cyB0byBzb21lIGNsdXN0ZXIgYWNjb3JkaW5nIHRvIHRoZSBzbWFsbGVzdCBkaXZlcmdlbmNlIGJldHdlZW4gdGhlIGRhdGEgcG9pbnQgYW5kIHRoZSBjZW50cm9pZC4gSXQgcHJvdmlkZXMgYSB2ZWN0b3Igb2YgY2x1c3RlcnMgb2YgYWxsIHRoZSBkYXRhIHBvaW50cy4gRnJvbSB0aGVyZSwgZnVuY3Rpb24gYG5ld0NlbnRyb2lkc2AgY29tcHV0ZXMgbmV3IGNlbnRyb2lkcyBnaXZlbiB0aGUgY2x1c3RlciBsYWJlbHMgb2YgYWxsIGRhdGEgcG9pbnRzLg0KDQotICoqQXJndW1lbnQqKjoNCg0KICAgIC0gYHguYCA6IHRoZSBkYXRhIG1hdHJpY2VzLCB0aWJibGVzIGFuZCBkYXRhIGZyYW1lcywgY29udGFpbmluZyB0aGUgZGF0YSBwb2ludHMgdG8gYmUgYXNzaWduZWQgdG8gc29tZSBjbHVzdGVyLg0KICAgIC0gYGNlbnRyb2lkc2AgOiB0aGUgbWF0cml4IG9yIGRhdGEgZnJhbWUgb2YgY2VudHJvaWRzIChieSByb3cpLg0KICAgIC0gYGRpdmAgOiB0aGUgZGl2ZXJnZW5jZSB0eXBlIHRvIGJlIHVzZWQuDQogICAgLSBgZGVnYCA6IHRoZSBkZWdyZWUgb2YgcG9seW5vbWlhbCBCRCAoaWYgb25lIGlzIHVzZWQpLg0KICAgIA0KLSAqKlZhbHVlKio6IA0KDQogICAgVGhlIGVhY2ggb2YgdGhlIHR3byBmdW5jdGlvbnMgcmV0dXJucyBhcmd1bWVudHMgZm9yIG9uZSBhbm90aGVyOg0KICAgIA0KICAgIC0gYGZpbmRDbG9zZXN0Q2VudHJvaWRgIHJldHVybnMgYSB2ZWN0b3Igb2Ygc2l6ZSBlcXVhbHMgdG8gdGhlIG51bWJlciBvZiByb3dzIG9mIGRhdGEgbWF0cml4IHguLCBjb250YWluaW5nIHRoZSBjbHVzdGVyIGxhYmVscyBvZiB0aGUgZGF0YSBwb2ludHMuDQogICAgLSBgbmV3Q2VudHJvaWRzYCByZXR1cm5zIG5ldyBtYXRyaXggb2YgY2VudHJvaWRzLg0KDQpgYGB7cn0NCmZpbmRDbG9zZXN0Q2VudHJvaWQgPC0gZnVuY3Rpb24oeC4sIGNlbnRyb2lkcy4sIGRpdiwgZGVnID0gMyl7DQogIGRpc3QgPC0gQnJlZ21hbkRpdih4LiwgY2VudHJvaWRzLiwgZGl2LCBkZWcpDQogIGNsdXN0IDwtIDE6bnJvdyh4LikgJT4lDQogICAgbWFwX2ludCguZiA9IH4gd2hpY2gubWluKGRpc3RbLngsXSkpDQogIHJldHVybihjbHVzdCkNCn0NCm5ld0NlbnRyb2lkcyA8LSBmdW5jdGlvbih4LiwgY2x1c3RlcnMuKXsNCiAgY2VudHJvaWRzIDwtIHVuaXF1ZShjbHVzdGVycy4pICU+JQ0KICAgIG1hcF9kZnIoLmYgPSB+IGNvbE1lYW5zKHguW2NsdXN0ZXJzLiA9PSAueCwgXSkpDQogIHJldHVybihjZW50cm9pZHMpDQp9DQpgYGANCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+PHU+RnVuY3Rpb248L3U+PC9zcGFuPiA6IGBrbWVhbnNCRGANCi0tLS0NCg0KVGhpcyBmdW5jdGlvbiBwZXJmb3JtcyAkSyQtbWVhbnMgYWxnb3JpdGhtIHdpdGggQkRzLiANCg0KLSAqKkFyZ3VtZW50Kio6DQoNCiAgICAtIGB0cmFpbl9pbnB1dGAgOiB0aGUgZGF0YSBtYXRyaWNlcywgdGliYmxlcyBhbmQgZGF0YSBmcmFtZXMsIGNvbnRhaW5pbmcgdGhlIGRhdGEgcG9pbnRzLg0KICAgIC0gYEtgIDogdGhlIG51bWJlciBvZiBjbHVzdGVycy4NCiAgICAtIGBuX3N0YXJ0YCA6IHRoZSBudW1iZXIgb2YgdGltZXMgdG8gcGVyZm9ybSB0aGUgYWxnb3JpdGhtLCBhbmQgdGhlIGJlc3Qgb25lIGFtb25nIHRoZW0gaXMgY2hvc2VuIHRvIGJlIHRoZSBmaW5hbCByZXN1bHQuIFRoaXMgaXMgZG9uZSB0byBhdm9pZCBsb2NhbCBvcHRpbWFsIHNvbHV0aW9ucy4gQnkgZGVmYXVsdCwgYG5fc3RhcnQgPSA1YC4NCiAgICAtIGBtYXhJdGVyYCA6IHRoZSBtYXhpbXVtIG51bWJlciBvZiBpdGVyYXRpb25zIGluIGNhc2UgdGhlIGFsZ29yaXRobSBkb2VzIG5vdCBjb252ZXJnZS4gQnkgZGVmYXVsdCwgYG1heEl0ZXIgPSA1MDBgLg0KICAgIC0gYGRlZ2AgOiB0aGUgZGVncmVlIG9mIHBvbHlub21pYWwgQkQgKGlmIG9uZSBpcyB1c2VkKS4NCiAgICAtIGBzY2FsZV9pbnB1dGAgOiBhIGxvZ2ljYWwgdmFsdWUgY29udHJvbGxpbmcgd2hldGhlciB0byBzY2FsZSB0aGUgaW5wdXQgdG8gYmUgaW4gJCgwLDEpJCBvciBub3QuIEJ5IGRlZmF1bHQsIGBzY2FsZV9pbnB1dCA9IEZBTFNFYC4NCiAgICAtIGBkaXZgIDogdGhlIHR5cGUgb2YgZGl2ZXJnZW5jZSB0byBiZSB1c2VkLiBCeSBkZWZhdWx0LCBgZGl2ID0gImV1Y2xpZGVhbiJgIGFuZCB0aGUgdXN1YWwgJEskLW1lYW5zIGFsZ29yaXRobSBpcyBwZXJmb3JtZWQuDQogICAgLSBgc3BsaXRzYCA6IGEgcmVhbCBudW1iZXIgYmV0d2VlbiAkMCQgYW5kICQxJCBzcGVjaWZ5aW5nIHRoZSBwcm9wb3J0aW9uIG9mIHRyYWluaW5nIGRhdGEgdG8gYmUgdXNlZCB0byBwZXJmb3JtICRLJC1tZWFucyBhbGdvcml0aG0uIFRoZSByZW1haW5pbmcgcGFydCB3aXRoIGJlIHVzZWQgZm9yIHRoZSBhZ2dyZWdhdGlvbi4gQnkgZGVmYXVsdCwgYHNwbGl0cyA9IDFgIGFuZCBhbGwgdGhlIGlucHV0IGRhdGEgYXJlIHVzZWQuDQogICAgLSBgZXBzaWxvbmAgOiB0aGUgc3RvcHBpbmcgY3JpdGVyaW9uIG9mIHRoZSBhbGdvcml0aG0uIEJ5IGRlZmF1bHQsIGBlcHNpbG9uID0gMWUtMTBgLg0KICAgIC0gYGNlbnRlcl9gLCBgc2NhbGVfYCA6IHRoZSBjZW50ZXIgYW5kIHNjYWxlIHRvIGJlIHVzZWQgdG8gc2NhbGUgdGhlIGlucHV0IGRhdGEuIEJ5IGRlZmF1bHQsIHRoZXkgYXJlIGBOVUxMYC4NCiAgICAtIGBpZF9zaHVmZmxlYCA6IGEgbG9naWNhbCB2ZWN0b3Igc3BlY2lmeWluZyB3aGljaCBwYXJ0IG9mIHRoZSB0cmFpbmluZyBkYXRhIHdpbGwgYmUgc2VsZWN0ZWQgdG8gcGVyZm9ybSB0aGUgYWxnb3JpdGhtLiBUaGlzIGlzIGltcG9ydGFudCB3aGVuIHdlIHdhbnQgdG8gcGVyZm9ybSB0aGUgYWxnb3JpdGhtIG9uIHRoZSBzYW1lIHNldCBvZiBkYXRhIHBvaW50cyBidXQgd2l0aCBkaWZmZXJlbnQgQkRzLiANCiAgICANCi0gKipWYWx1ZSoqOiANCg0KICAgIFRoaXMgZnVuY3Rpb24gcmV0dXJucyBhICoqbGlzdCoqIG9mIHRoZSBmb2xsb3dpbmcgb2JqZWN0czoNCiAgICAtIGBjZW50cm9pZHNgIDogdGhlIG1hdHJpeCBvZiB0aGUgY2VudHJvaWRzIG9idGFpbmVkIGJ5IHRoZSBhbGdvcml0aG0uDQogICAgLSBgY2x1c3RlcnNgIDogYSB2ZWN0b3Igb2YgY2x1c3RlciBsYWJlbHMgb2YgdGhlIGRhdGEgcG9pbnRzLg0KICAgIC0gYHRyYWluX2RhdGFgIDogYSBsaXN0IG9mIHRoZSBmb2xsb3dpbmcgb2JqZWN0czoNCiAgICAgICAgLSBgWF90cmFpbmAgOiB0aGUgdHJhaW5pbmcgZGF0YSB1c2VkIGZvciB0aGUgYWxnb3JpdGhtLg0KICAgICAgICAtIGBYX3JlbWFpbmAgOiB0aGUgcmVtYWluaW5nIHBhcnQgb2YgdGhlIGlucHV0IGRhdGEgdXNlZCBmb3IgdGhlIGFnZ3JlZ2F0aW9uLg0KICAgICAgICAtIGBpZF9yZW1haW5gIDogYSBsb2dpY2FsIHZlY3RvciBzcGVjaWZ5aW5nIHRoZSByZW1haW5pbmcgcGFydCAoYFhfcmVtYWluYCkgb2YgdGhlIGlucHV0IGRhdGEuDQogICAgLSBgcGFyYW1ldGVyc2AgOiBhIGxpc3Qgb2YgdGhlIGZvbGxvd2luZyBvYmplY3RzOg0KICAgICAgICAtIGBkaXZgIDogZGl2ZXJnZW5jZSB1c2VkLg0KICAgICAgICAtIGBkZWdgIDogdGhlIGRlZ3JlZSBvZiBwb2x5bm9taWFsIEJEIChpZiBvbmUgaXMgdXNlZCkuDQogICAgICAgIC0gYGNlbnRlcl9gLCBgc2NhbGVfYCA6IHRoZSBjZW50ZXIgYW5kIHNjYWxlIHVzZWQgdG8gc2NhbGUgdGhlIGlucHV0IGRhdGEuDQogICAgLSBgcnVubmluZ190aW1lYDogdGhlIGNvbXB1dGF0aW9uYWwgdGltZSBvZiB0aGUgYWxnb3JpdGhtLg0KDQpgYGB7cn0NCmttZWFuc0JEIDwtIGZ1bmN0aW9uKHRyYWluX2lucHV0LA0KICAgICAgICAgICAgICAgICAgICAgSywNCiAgICAgICAgICAgICAgICAgICAgIG5fc3RhcnQgPSA1LA0KICAgICAgICAgICAgICAgICAgICAgbWF4SXRlciA9IDUwMCwNCiAgICAgICAgICAgICAgICAgICAgIGRlZyA9IDMsDQogICAgICAgICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgZGl2ID0gImV1Y2xpZGVhbiIsDQogICAgICAgICAgICAgICAgICAgICBzcGxpdHMgPSAxLA0KICAgICAgICAgICAgICAgICAgICAgZXBzaWxvbiA9IDFlLTEwLA0KICAgICAgICAgICAgICAgICAgICAgY2VudGVyXyA9IE5VTEwsDQogICAgICAgICAgICAgICAgICAgICBzY2FsZV8gPSBOVUxMLA0KICAgICAgICAgICAgICAgICAgICAgaWRfc2h1ZmZsZSA9IE5VTEwpew0KICBzdGFydF90aW1lIDwtIFN5cy50aW1lKCkNCiAgIyBEaXN0b3J0aW9uIGZ1bmN0aW9uDQogIFggPC0gYXMubWF0cml4KHRyYWluX2lucHV0KQ0KICBOIDwtIGRpbShYKQ0KICBpZihzY2FsZV9pbnB1dCl7DQogICAgaWYoIShpcy5udWxsKGNlbnRlcl8pICYgaXMubnVsbChzY2FsZV8pKSl7DQogICAgICBpZihsZW5ndGgoY2VudGVyXykgPT0gMSl7DQogICAgICAgIGNlbnRlcl8gPC0gcmVwKGNlbnRlcl8sIE5bMl0pDQogICAgICB9DQogICAgICBpZihsZW5ndGgoc2NhbGVfKSA9PSAxKXsNCiAgICAgICAgc2NhbGVfIDwtIHJlcChzY2FsZV8sIE5bMl0pDQogICAgICB9DQogICAgfSBlbHNlew0KICAgICAgbWluXyA8LSBhcHBseShYLCAyLCBGVU4gPSBtaW4pDQogICAgICBjXyA8LSBhYnMoY29sTWVhbnMoWCkvNSkNCiAgICAgIGNlbnRlcl8gPC0gbWluXyAtIGNfDQogICAgICBzY2FsZV8gPC0gYXBwbHkoWCwgMiwgRlVOID0gbWF4KSAtIGNlbnRlcl8gKyAxDQogICAgfQ0KICAgIFggPC0gc2NhbGUoWCwgY2VudGVyID0gY2VudGVyXywgc2NhbGUgPSBzY2FsZV8pDQogIH0NCiAgaWYoaXMubnVsbChpZF9zaHVmZmxlKSl7DQogICAgdHJhaW5faWQgPC0gcmVwKFRSVUUsIE5bMV0pDQogICAgaWYoc3BsaXRzIDwgMSl7DQogICAgICB0cmFpbl9pZFtzYW1wbGUoTlsxXSwgZmxvb3IoTlsxXSooMS1zcGxpdHMpKSldIDwtIEZBTFNFDQogICAgfQ0KICB9IGVsc2V7DQogICAgdHJhaW5faWQgPC0gaWRfc2h1ZmZsZQ0KICB9DQogIFhfdHJhaW4xIDwtIFhbdHJhaW5faWQsXQ0KICBYX3RyYWluMiA8LSBYWyF0cmFpbl9pZCxdDQogIG11IDwtIGFzLm1hdHJpeChjb2xNZWFucyhYX3RyYWluMSkpDQogIGRpc3RvcnRpb24gPC0gZnVuY3Rpb24oY2x1cyl7DQogICAgY2VudCA8LSBuZXdDZW50cm9pZHMoWF90cmFpbjEsIGNsdXMpDQogICAgdmFyX3dpdGhpbiA8LSAxOksgJT4lDQogICAgICBtYXAoLmYgPSB+IEJyZWdtYW5EaXYoWF90cmFpbjFbY2x1cyA9PSAueCxdLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBjZW50Wy54LF0sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRpdiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgZGVnKSkgJT4lDQogICAgICBtYXAoLmYgPSBzdW0pICU+JQ0KICAgICAgUmVkdWNlKCIrIiwgLikNCiAgICByZXR1cm4odmFyX3dpdGhpbikNCiAgfQ0KICAjIEttZWFucyBhbGdvcml0aG0NCiAga21lYW5zV2l0aEJEIDwtIGZ1bmN0aW9uKHguLCBrLiwgbWF4aXRlci4sIGVwcy4pIHsNCiAgICBuLiA8LSBucm93KHguKQ0KICAgICMgaW5pdGlhbGl6YXRpb24NCiAgICBpbml0IDwtIHNhbXBsZShuLiwgay4pDQogICAgY2VudHJvaWRzX29sZCA8LSB4Lltpbml0LF0NCiAgICBpIDwtIDANCiAgICB3aGlsZShpIDwgbWF4SXRlcil7DQogICAgICAjIEFzc2lnbm1lbnQgc3RlcA0KICAgICAgY2x1c3RlcnMgPC0gZmluZENsb3Nlc3RDZW50cm9pZCh4LiwgY2VudHJvaWRzX29sZCwgZGl2LCBkZWcpDQogICAgICAjIFJlY29tcHV0ZSBjZW50cm9pZHMNCiAgICAgIGNlbnRyb2lkc19uZXcgPC0gbmV3Q2VudHJvaWRzKHguLCBjbHVzdGVycykNCiAgICAgIGlmICgoc3VtKGlzLm5hKGNlbnRyb2lkc19uZXcpKSA+IDApIHwNCiAgICAgICAgICAobnJvdyhjZW50cm9pZHNfbmV3KSAhPSBrLikpIHsNCiAgICAgICAgaW5pdCA8LSBzYW1wbGUobi4sIGsuKQ0KICAgICAgICBjZW50cm9pZHNfb2xkIDwtIHguW2luaXQsXQ0KICAgICAgICB3YXJuaW5nKCJOQSBwcm9kdWNlZCAtPiByZWluaXRpYWxpemUgY2VudHJvaWRzLi4uISIpDQogICAgICB9DQogICAgICBlbHNlew0KICAgICAgICBpZihzdW0oYWJzKGNlbnRyb2lkc19uZXcgLSBjZW50cm9pZHNfb2xkKSkgPiBlcHMuKXsNCiAgICAgICAgICBjZW50cm9pZHNfb2xkIDwtIGNlbnRyb2lkc19uZXcNCiAgICAgICAgfSBlbHNlew0KICAgICAgICAgIGJyZWFrDQogICAgICAgIH0NCiAgICAgIH0NCiAgICAgIGkgPC0gaSArIDENCiAgICB9DQogICAgcmV0dXJuKGNsdXN0ZXJzKQ0KICB9DQogIHJlc3VsdHMgPC0gMTpuX3N0YXJ0ICU+JSANCiAgICBtYXBfZGZjKC5mID0gfiB0aWJibGUoInt7Lnh9fSIgOj0ga21lYW5zV2l0aEJEKFhfdHJhaW4xLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEssDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhJdGVyLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVwc2lsb24pKSkNCiAgb3B0X2lkIDwtIDE6bl9zdGFydCAlPiUNCiAgICBtYXBfZGJsKC5mID0gfiBkaXN0b3J0aW9uKHJlc3VsdHNbWy54XV0pKSAlPiUNCiAgICB3aGljaC5taW4NCiAgY2x1c3RlciA8LSBjbHVzdGVycyA8LSByZXN1bHRzW1tvcHRfaWRdXQ0KICBqIDwtIDENCiAgSUQgPC0gdW5pcXVlKGNsdXN0ZXIpDQogIGZvciAoaSBpbiBJRCkgew0KICAgIGNsdXN0ZXJzW2NsdXN0ZXIgPT0gaV0gPSBqDQogICAgaiA9ICBqICsgMQ0KICB9DQogIGNlbnRyb2lkcyA9IG5ld0NlbnRyb2lkcyhYX3RyYWluMSwgY2x1c3RlcnMpDQogIHRpbWVfdGFrZW4gPC0gU3lzLnRpbWUoKSAtIHN0YXJ0X3RpbWUNCiAgcmV0dXJuKA0KICAgIGxpc3QoDQogICAgICBjZW50cm9pZHMgPSBjZW50cm9pZHMsDQogICAgICBjbHVzdGVycyA9IGNsdXN0ZXJzLA0KICAgICAgdHJhaW5fZGF0YSA9IGxpc3QoWF90cmFpbiA9IFhfdHJhaW4xLA0KICAgICAgICAgICAgICAgICAgICAgICAgWF9yZW1haW4gPSBYX3RyYWluMiwNCiAgICAgICAgICAgICAgICAgICAgICAgIGlkX3JlbWFpbiA9ICF0cmFpbl9pZCksDQogICAgICBwYXJhbWV0ZXJzID0gbGlzdChkaXYgPSBkaXYsDQogICAgICAgICAgICAgICAgICAgICAgICBkZWcgPSBkZWcsDQogICAgICAgICAgICAgICAgICAgICAgICBjZW50ZXJfID0gY2VudGVyXywNCiAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlXyA9IHNjYWxlXyksDQogICAgICBydW5uaW5nX3RpbWUgPSB0aW1lX3Rha2VuDQogICAgKQ0KICApDQp9DQpgYGANCg0KLS0tLQ0KDQo+ICoqRXhhbXBsZS4xKio6IFdlIHBlcmZvcm0gJEskLW1lYW5zIGFsZ29yaXRobSB3aXRoIGAiZ2tsImAgQkQgb24gW0FiYWxvbmVdKGh0dHBzOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9tYWNoaW5lLWxlYXJuaW5nLWRhdGFiYXNlcy9hYmFsb25lKSBkYXRhc2V0Lg0KDQotLS0tDQoNCmBgYHtyfQ0KcGFjbWFuOjpwX2xvYWQocmVhZHIpDQpjb2xuYW1lIDwtIGMoIlR5cGUiLCAiTG9uZ2VzdFNoZWxsIiwgIkRpYW1ldGVyIiwgIkhlaWdodCIsICJXaG9sZVdlaWdodCIsICJTaHVja2VkV2VpZ2h0IiwgIlZpc2NlcmFXZWlnaHQiLCAiU2hlbGxXZWlnaHQiLCAiUmluZ3MiKQ0KZGYgPC0gcmVhZHI6OnJlYWRfZGVsaW0oImh0dHBzOi8vYXJjaGl2ZS5pY3MudWNpLmVkdS9tbC9tYWNoaW5lLWxlYXJuaW5nLWRhdGFiYXNlcy9hYmFsb25lL2FiYWxvbmUuZGF0YSIsIGNvbF9uYW1lcyA9IGNvbG5hbWUsIGRlbGltID0gIiwiLCBzaG93X2NvbF90eXBlcyA9IEZBTFNFKQ0KbiA8LSBucm93KGRmKQ0KdHJhaW4gPC0gbG9naWNhbChuKQ0KdHJhaW5bc2FtcGxlKG4sICBmbG9vcihuKjAuOCkpXSA8LSBUUlVFDQpjbCA8LSBkZlt0cmFpbiwyOihuY29sKGRmKS0xKV0gJT4lDQogIGttZWFuc0JEKEsgPSAzLCBkaXYgPSAiZ2tsIiwgc3BsaXRzID0gMC41LCBzY2FsZV9pbnB1dCA9IFRSVUUpDQp0YWJsZShjbCRjbHVzdGVycykNCmBgYA0KDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogIzFGQUFFMzsiPjx1PlN0ZXAgJEYkOiBGaXR0aW5nIHByZWRpY3RpdmUgbW9kZWxzPC91Pjwvc3Bhbj4NCj09PQ0KDQpUaGlzIHNlY3Rpb24gYnVpbGRzIGdsb2JhbCBtb2RlbHMgYnkgZml0dGluZyBsb2NhbCBtb2RlbCBvbiBlYWNoIGdpdmVuIGNsdXN0ZXIgb2YgdGhlIG9idGFpbmVkIHBhcnRpdGlvbi4gVGhpcyBjb3JyZXNwb25kcyB0byB0aGUgc3RlcCAkRiQgb2YgdGhlIHByb2NlZHVyZS4NCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjRjBBRTE0OyI+PHU+RnVuY3Rpb248L3U+PC9zcGFuPiA6IGBmaXRMb2NhbE1vZGVsc2ANCi0tLS0NCg0KVGhpcyBmdW5jdGlvbiBmaXRzIGxvY2FsIG1vZGVscyAkKHtcY2FsIE1fe2osa319KV97aixrfSQgb24gYWxsIGNsdXN0ZXJzICRrPTEsLi4uLEskIG9mIHRoZSBvYnRhaW5lZCBwYXJ0aXRpb24sIGdpdmVuIGJ5IEJyZWdtYW4gZGl2ZXJnZW5jZSAke1xjYWwgQn1faiQsIGZvciAkaj0xLC4uLixNJC4NCg0KLSAqKkFyZ3VtZW50Kio6DQoNCiAgICAtIGBrbWVhbnNfQkRgIDogYW4gb2JqZWN0IG9idGFpbmVkIGZyb20gYGttZWFuc0JEYCBmdW5jdGlvbi4NCiAgICAtIGB0cmFpbl9yZXNwb25zZWAgOiB0aGUgdmVjdG9yIG9mIHJlc3BvbnNlIHZhcmlhYmxlIGNvcnJlc3BvbmRpbmcgdG8gdGhlIGZ1bGwgYGlucHV0X2RhdGFgIGdpdmVuIHRvIGBrbWVhbkJEYCBmdW5jdGlvbi4NCiAgICAtIGBtb2RlbGAgOmEgbG9jYWwgbW9kZWwgdHlwZSB0byBmaXQgb24gYWxsIHRoZSBjbHVzdGVycyBvZiB0aGUgZ2l2ZW4gcGFydGl0aW9uLiBJdCBzaG91bGQgYmUgZWl0aGVyIGEgbW9kZWwgb2JqZWN0ICh3b3JrcyB3aXRoIGZ1bmN0aW9uIGBwcmVkaWN0YCksIG9yIGEgc3RyaW5nIGVsZW1lbnQgb2YgeyJsbSIsICJ0cmVlIiwgInJmIn0gd2hpY2ggY29ycmVzcG9uZHMgdG8gbGluZWFyIHJlZ3Jlc3Npb24sIHRyZWUgYW5kIHJhbmRvbSBmb3Jlc3QgcmVzcGVjdGl2ZWx5LiBCeSBkZWZhdWx0LCBgbW9kZWwgPSAibG0iYC4NCiAgICAtIGBmb3JtdWxhYCA6IHRoZSBkZWdyZWUgb2YgcG9seW5vbWlhbCBCRCAoaWYgb25lIGlzIHVzZWQpLg0KDQotICoqVmFsdWUqKjoNCg0KICAgIFRoaXMgZnVuY3Rpb24gcmV0dXJucyBhIGxpc3Qgb2YgdGhlIGZvbGxvd2luZyBvYmplY3RzOg0KICAgIC0gYGxvY2FsX21vZGVsc2AgOiBhbGwgdGhlIGxvY2FsIG1vZGVscyBmaXR0ZWQgb24gYWxsIGNsdXN0ZXJzIG9mIHRoZSBnaXZlbiBwYXJ0aXRpb24uDQogICAgLSBga21lYW5zX0JEYCA6IHRoZSBga21lYW5zQkRgIG9iamVjdC4NCiAgICAtIGBkYXRhX3JlbWFpbmAgOiBhIGxpc3Qgb2YgdGhlIGZvbGxvd2luZyBvYmplY3RzOg0KICAgICAgICAtIGBmaXRgIDogdGhlIGZpdHRlZCB2YWx1ZXMgb2YgdGhlIHJlbWFpbmluZyBwYXJ0IG9mIHRoZSBpbnB1dCBkYXRhLg0KICAgICAgICAtIGByZXNwb25zZWAgOiB0aGUgYWN0dWFsIHJlc3BvbnNlIHZhbHVlcyBjb3JyZXNwb25kaW5nIHRvIHRoZSByZW1haW5pbmcgaW5wdXQgZGF0YS4NCiAgICAtIGBydW5uaW5nX3RpbWVgIDogdGhlIGNvbXB1dGF0aW9uYWwgdGltZSBvZiB0aGUgYWxnb3JpdGhtLg0KDQpgYGB7cn0NCmZpdExvY2FsTW9kZWxzIDwtIGZ1bmN0aW9uKGttZWFuc19CRCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluX3Jlc3BvbnNlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgbW9kZWwgPSAibG0iLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgZm9ybXVsYSA9IE5VTEwpew0KICBzdGFydF90aW1lIDwtIFN5cy50aW1lKCkNCiAgWF90cmFpbiA8LSBrbWVhbnNfQkQkdHJhaW5fZGF0YSRYX3RyYWluDQogIHlfdHJhaW4gPC0gdHJhaW5fcmVzcG9uc2VbIShrbWVhbnNfQkQkdHJhaW5fZGF0YSRpZF9yZW1haW4pXQ0KICBYX3JlbWFpbiA8LSBrbWVhbnNfQkQkdHJhaW5fZGF0YSRYX3JlbWFpbg0KICB5X3JlbWFpbiA8LSBOVUxMDQogIGlmKCFpcy5udWxsKFhfcmVtYWluKSl7DQogICAgeV9yZW1haW4gPC0gdHJhaW5fcmVzcG9uc2Vba21lYW5zX0JEJHRyYWluX2RhdGEkaWRfcmVtYWluXQ0KICB9DQogIHBhY21hbjo6cF9sb2FkKHRyZWUpDQogIHBhY21hbjo6cF9sb2FkKHJhbmRvbUZvcmVzdCkNCiAgbW9kZWxfIDwtIGlmZWxzZShtb2RlbCA9PSAidHJlZSIsIHRyZWU6OnRyZWUsIG1vZGVsKQ0KICBLIDwtIG5yb3coa21lYW5zX0JEJGNlbnRyb2lkcykNCiAgaWYgKGlzLm51bGwoZm9ybXVsYSkpew0KICAgIGZvcm0gPC0gZm9ybXVsYSh0YXJnZXQgfiAuKQ0KICB9DQogIGVsc2V7DQogICAgZm9ybSA8LSB1cGRhdGUoZm9ybXVsYSwgdGFyZ2V0IH4gLikNCiAgfQ0KICBkYXRhXyA8LSBiaW5kX2NvbHMoWF90cmFpbiwgInRhcmdldCI6PSB5X3RyYWluKQ0KICBmaXRfbG9va3VwIDwtIGxpc3QobG0gPSAiZml0dGVkLnZhbHVlcyIsDQogICAgICAgICAgICAgICAgICAgICByZiA9ICJwcmVkaWN0ZWQiKQ0KICBpZihpcy5jaGFyYWN0ZXIobW9kZWxfKSl7DQogICAgbW9kZWxfbG9va3VwIDwtIGxpc3QobG0gPSBsbSwNCiAgICAgICAgICAgICAgICAgICAgICAgICByZiA9IHJhbmRvbUZvcmVzdDo6cmFuZG9tRm9yZXN0KQ0KICAgIG1vZCA8LSBtYXAoLnggPSAxOkssIA0KICAgICAgICAgICAgICAgLmYgPSB+IG1vZGVsX2xvb2t1cFtbbW9kZWxfXV0oZm9ybXVsYSA9IGZvcm0sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YSA9IGRhdGFfW2ttZWFuc19CRCRjbHVzdGVycyA9PSAueCwgXSkpDQogIH0gZWxzZXsNCiAgICBtb2QgPC0gbWFwKC54ID0gMTpLLCANCiAgICAgICAgICAgICAgIC5mID0gfiBtb2RlbF8oZm9ybXVsYSA9IGZvcm0sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gZGF0YV9ba21lYW5zX0JEJGNsdXN0ZXJzID09IC54LF0pKQ0KICB9DQogIHByZWQwIDwtIE5VTEwNCiAgaWYoIWlzLm51bGwoWF9yZW1haW4pKXsNCiAgICBwcmVkMCA8LSB2ZWN0b3IobW9kZSA9ICJudW1lcmljIiwgDQogICAgICAgICAgICAgICAgICAgIGxlbmd0aCA9IGxlbmd0aCh5X3JlbWFpbikpDQogICAgY2x1cyA8LSBmaW5kQ2xvc2VzdENlbnRyb2lkKHguID0gWF9yZW1haW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNlbnRyb2lkcy4gPSBrbWVhbnNfQkQkY2VudHJvaWRzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkaXYgPSBrbWVhbnNfQkQkcGFyYW1ldGVycyRkaXYsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRlZyA9IGttZWFuc19CRCRwYXJhbWV0ZXJzJGRlZykNCiAgICBmb3IoaV8gaW4gMTpLKXsNCiAgICAgIHByZWQwW2NsdXMgPT0gaV9dIDwtIHByZWRpY3QobW9kW1tpX11dLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhcy5kYXRhLmZyYW1lKFhfcmVtYWluW2NsdXMgPT0gaV8sXSkpDQogICAgfQ0KICB9DQogIHRpbWVfdGFrZW4gPC0gU3lzLnRpbWUoKSAtIHN0YXJ0X3RpbWUNCiAgcmV0dXJuKGxpc3QoDQogICAgbG9jYWxfbW9kZWxzID0gbW9kLA0KICAgIGttZWFuc19CRCA9IGttZWFuc19CRCwNCiAgICBkYXRhX3JlbWFpbiA9IGxpc3QoZml0ID0gcHJlZDAsDQogICAgICAgICAgICAgICAgICAgICAgIHJlc3BvbnNlID0geV9yZW1haW4pLA0KICAgIHJ1bm5pbmdfdGltZSA9IHRpbWVfdGFrZW4NCiAgKSkNCn0NCmBgYA0KDQotLS0tIA0KDQo+ICoqRXhhbXBsZS4yKio6IEZyb20gKipFeGFtcGxlLjEqKiBhYm92ZSwgbXVsdGlwbGUgbGluZWFyIHJlZ3Jlc3Npb24gbW9kZWxzIGFyZSBidWlsdCBvbiBhbGwgdGhlIG9idGFpbmVkIGNsdXN0ZXJzLiBUaGUgbWVhbiBzcXVhcmUgZXJyb3Igb2YgdGhpcyBtb2RlbCwgZXZhbHVhdGVkIG9uIHRoZSByZW1haW5pbmcgJDUwXCUkIG9mIHRoZSB0cmFpbmluZyBkYXRhIGlzIGNvbXB1dGVkLg0KDQotLS0tDQoNCmBgYHtyfQ0KZml0IDwtIGZpdExvY2FsTW9kZWxzKHRyYWluX3Jlc3BvbnNlID0gZGYkUmluZ3NbdHJhaW5dLA0KICAgICAgICAgICAgICAgICAgICAgIGttZWFuc19CRCA9IGNsLA0KICAgICAgICAgICAgICAgICAgICAgIG1vZGVsID0gImxtIikNCg0KbWVhbigoZml0JGRhdGFfcmVtYWluJHJlc3BvbnNlLSBmaXQkZGF0YV9yZW1haW4kZml0KV4yKQ0KYGBgDQoNCjxzcGFuIHN0eWxlPSJjb2xvcjogI0YwQUUxNDsiPjx1PkZ1bmN0aW9uPC91Pjwvc3Bhbj4gOiBgbG9jYWxQcmVkaWN0YA0KLS0tLQ0KDQpUaGlzIGZ1bmN0aW9uIGFsbG93cyB1cyB0byBwcmVkaWN0IGFueSBuZXcgb2JzZXJ2YXRpb25zIHVzaW5nIHRoZSBjYW5kaWRhdGUgbW9kZWwgJHtcY2FsIE19X2o9KHtcY2FsIE19X3tqLGt9KV97az0xfV5NJCBjb3JyZXNwb25kaW5nIHRvIEJyZWdtYW4gZGl2ZXJnZW5jZSAke1xjYWwgQn1faiQsIGZvciBzb21lICRqXGluIEpcc3Vic2V0XHsxLC4uLixNXH0kLiANCg0KLSAqKkFyZ3VtZW50Kio6DQoNCiAgICAtIGBsb2NhbE1vZGVsc2AgOiBhIGxvY2FsIG1vZGVsIG9iamVjdCBvYnRhaW5lZCBmcm9tIGBmaXRMb2NhbE1vZGVsc2AgZnVuY3Rpb24uDQogICAgLSBgbmV3RGF0YWAgOiBuZXcgaW5wdXQgZGF0YSB0byBiZSBwcmVkaWN0ZWQgdXNpbmcgdGhlIGNhbmRpZGF0ZSBtb2RlbHMgZ2l2ZW4gaW4gYGxvY2FsTW9kZWxzYCBhcmd1bWVudC4NCiAgICANCi0gKipWYWx1ZSoqOg0KDQogICAgVGhpcyBmdW5jdGlvbiByZXR1cm5zIGEgcHJlZGljdGVkIHZlY3RvciBvZiB0aGUgYG5ld0RhdGFgLg0KDQpgYGB7cn0NCmxvY2FsUHJlZGljdCA8LSBmdW5jdGlvbihsb2NhbE1vZGVscywNCiAgICAgICAgICAgICAgICAgICAgICAgICBuZXdEYXRhKXsNCiAga21lYW5fQkQgPC0gbG9jYWxNb2RlbHMka21lYW5zX0JEDQogIEsgPC0gbnJvdyhrbWVhbl9CRCRjZW50cm9pZHMpDQogIG5ld0RhdGFfIDwtIG5ld0RhdGENCiAgaWYoIShpcy5udWxsKGttZWFuX0JEJHBhcmFtZXRlcnMkY2VudGVyXykpKXsNCiAgICBuZXdEYXRhXyA8LSBzY2FsZShuZXdEYXRhLA0KICAgICAgICAgICAgICAgICAgICAgIGNlbnRlciA9IGttZWFuX0JEJHBhcmFtZXRlcnMkY2VudGVyXywNCiAgICAgICAgICAgICAgICAgICAgICBzY2FsZSA9IGttZWFuX0JEJHBhcmFtZXRlcnMkc2NhbGVfKQ0KICAgIGlkMCA8LSAobmV3RGF0YV8gPD0gMCkNCiAgICBpZihzdW0oaWQwKSA+IDApew0KICAgICAgbWluXyA8LSBtaW4obmV3RGF0YV9baWQwXSkNCiAgICAgIG5ld0RhdGFfW2lkMF0gPC0gcnVuaWYoc3VtKGlkMCksIG1pbigxZS0zLCBtaW5fLzEwKSwgbWluXykNCiAgICB9DQogIH0NCiAgY2x1cyA8LSBmaW5kQ2xvc2VzdENlbnRyb2lkKHguID0gbmV3RGF0YV8sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjZW50cm9pZHMuID0ga21lYW5fQkQkY2VudHJvaWRzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGl2ID0ga21lYW5fQkQkcGFyYW1ldGVycyRkaXYsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZWcgPSBrbWVhbl9CRCRwYXJhbWV0ZXJzJGRlZykNCiAgcHJlZDAgPC0gdmVjdG9yKG1vZGUgPSAibnVtZXJpYyIsIGxlbmd0aCA9IG5yb3cobmV3RGF0YV8pKQ0KICBmb3IoaV8gaW4gMTpLKXsNCiAgICBwcmVkMFtjbHVzID09IGlfXSA8LSBwcmVkaWN0KGxvY2FsTW9kZWxzJGxvY2FsX21vZGVsc1tbaV9dXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFzLmRhdGEuZnJhbWUobmV3RGF0YV9bY2x1cyA9PSBpXyxdKSkNCiAgfQ0KICBwcmVkMCA8LSBhc190aWJibGUocHJlZDApDQogIG5hbWVzKHByZWQwKSA8LSBpZmVsc2Uoa21lYW5fQkQkcGFyYW1ldGVycyRkaXYgPT0gInBvbHlub21pYWwiLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3RlMCgicG9seW5vbWlhbCIsIGttZWFuX0JEJHBhcmFtZXRlcnMkZGVnKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBrbWVhbl9CRCRwYXJhbWV0ZXJzJGRpdikNCiAgcmV0dXJuKHByZWQwKQ0KfQ0KYGBgDQoNCi0tLS0NCg0KPiAqKkV4YW1wbGUuMyoqOiBUaGUgcGVyZm9ybWFuY2Ugb2YgdGhlIGNhbmRpZGF0ZSBtb2RlbCBjb3JyZXNwb25kaW5nIHRvIGAiZ2tsImAgZGl2ZXJnZW5jZSBpcyBjb21wYXJlZCB0byByYW5kb20gZm9yZXN0IHJlZ3Jlc3Npb24gb24gYSAkMjBcJSQgdGVzdGluZyBkYXRhLg0KDQotLS0tDQoNCmBgYHtyfQ0KeV9oYXQgPC0gbG9jYWxQcmVkaWN0KGZpdCwNCiAgICAgICAgICAgICAgICAgICAgICBkZlshdHJhaW4sIDI6KG5jb2woZGYpLTEpLF0pDQpyZiA8LSByYW5kb21Gb3Jlc3QoUmluZ3MgfiAuLCBkYXRhID0gZGZbdHJhaW4sMjpuY29sKGRmKV0pDQptZWFuKChwcmVkaWN0KHJmLCBuZXdkYXRhID0gZGZbIXRyYWluLDI6bmNvbChkZildKS0gZGYkUmluZ3NbIXRyYWluXSleMikNCm1lYW4oKHlfaGF0JGdrbC1kZiRSaW5nc1shdHJhaW5dKV4yKQ0KYGBgDQoNCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+PHU+U3RlcCAkQyQ6IENvbWJpbmluZyBtZXRob2RzPC91Pjwvc3Bhbj4NCj09PQ0KDQpUaGUgc291cmNlIGNvZGVzIGFuZCBpbmZvcm1hdGlvbiBvZiB0aGUgYWdncmVnYXRpb24gbWV0aG9kcyBhcmUgYXZhaWxhYmxlIFtoZXJlIDxzcGFuIHN0eWxlPSJjb2xvcjogIzA5N0JDMSI+IGByIGZvbnRhd2Vzb21lOjpmYSgiZ2l0aHViIilgPC9zcGFuPl0oaHR0cHM6Ly9naXRodWIuY29tL2hhc3NvdGhlYS9BZ2dyZWdhdGlvbk1ldGhvZHMpLiBUaGUgY29kZXMgYmVsb3cgaW1wb3J0cyB0aGUgYWdncmVnYXRpb24gbWV0aG9kcyBpbnRvIDxzcGFuIHN0eWxlPSJjb2xvcjogIzAyODdEODsiPiAqKlJzdHVkaW8qKiA8L3NwYW4+IGVudmlyb25tZW50Lg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCnBhY21hbjo6cF9sb2FkKGRldnRvb2xzKQ0KIyMjIEtlcm5lbCBiYXNlZCBjb25zZW5zdWFsIGFnZ3JlZ2F0aW9uDQpzb3VyY2VfdXJsKCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vaGFzc290aGVhL0FnZ3JlZ2F0aW9uTWV0aG9kcy9tYWluL0tlcm5lbEFnZ1JlZy5SIikNCiMjIyBNaXhDb2JyYQ0Kc291cmNlX3VybCgiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2hhc3NvdGhlYS9BZ2dyZWdhdGlvbk1ldGhvZHMvbWFpbi9NaXhDb2JyYVJlZy5SIikNCmBgYA0KDQo8c3BhbiBzdHlsZT0iY29sb3I6ICNGMEFFMTQ7Ij48dT5GdW5jdGlvbjwvdT48L3NwYW4+IDogYHN0ZXBLYCwgYHN0ZXBGYCBhbmQgYHN0ZXBDYA0KLS0tLQ0KDQpUaGVzZSBmdW5jdGlvbnMgYWxsb3cgdG8gc2V0IHZhbHVlcyBvZiB0aGUgcGFyYW1ldGVycyBpbiB0aGUgdGhyZWUgc3RlcHMgb2YgdGhlIEtGQyBwcm9jZWR1cmUuIEVhY2ggZnVuY3Rpb24gcmV0dXJucyBhIGxpc3Qgb2YgYWxsIHRoZSBwYXJhbWV0ZXJzIGdpdmVuIGluIGl0cyBhcmd1bWVudHMuDQoNCg0KYGBge3J9DQpzdGVwSyA9IGZ1bmN0aW9uKEssDQogICAgICAgICAgICAgICAgIG5fc3RhcnQgPSA1LA0KICAgICAgICAgICAgICAgICBtYXhJdGVyID0gMzAwLA0KICAgICAgICAgICAgICAgICBkZWcgPSAzLA0KICAgICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICBkaXYgPSBOVUxMLA0KICAgICAgICAgICAgICAgICBzcGxpdHMgPSAwLjc1LA0KICAgICAgICAgICAgICAgICBlcHNpbG9uID0gMWUtMTAsDQogICAgICAgICAgICAgICAgIGNlbnRlcl8gPSBOVUxMLA0KICAgICAgICAgICAgICAgICBzY2FsZV8gPSBOVUxMKXsNCiAgcmV0dXJuKGxpc3QoSyA9IEssDQogICAgICAgICAgICAgIG5fc3RhcnQgPSBuX3N0YXJ0LA0KICAgICAgICAgICAgICBtYXhJdGVyID0gbWF4SXRlciwNCiAgICAgICAgICAgICAgZGVnID0gZGVnLA0KICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IHNjYWxlX2lucHV0LA0KICAgICAgICAgICAgICBkaXYgPSBkaXYsDQogICAgICAgICAgICAgIHNwbGl0cyA9IHNwbGl0cywNCiAgICAgICAgICAgICAgZXBzaWxvbiA9IGVwc2lsb24sDQogICAgICAgICAgICAgIGNlbnRlcl8gPSBjZW50ZXJfICwNCiAgICAgICAgICAgICAgc2NhbGVfID0gc2NhbGVfKSkNCn0NCg0Kc3RlcEYgPSBmdW5jdGlvbihmb3JtdWxhID0gTlVMTCwgDQogICAgICAgICAgICAgICAgIG1vZGVsID0gImxtIil7DQogIHJldHVybihsaXN0KGZvcm11bGEgPSBmb3JtdWxhLCANCiAgICAgICAgICAgICAgbW9kZWwgPSBtb2RlbCkpDQp9DQoNCnN0ZXBDID0gZnVuY3Rpb24obl9jdiA9IDUsDQogICAgICAgICAgICAgICAgIG1ldGhvZCA9IGMoImNvYnJhIiwgIm1peGNvYnJhIiksDQogICAgICAgICAgICAgICAgIG9wdF9tZXRob2RzID0gYygiZ3JhZCIsICJncmlkIiksDQogICAgICAgICAgICAgICAgIGtlcm5lbHMgPSAiZ2F1c3NpYW4iLA0KICAgICAgICAgICAgICAgICBzY2FsZV9mZWF0dXJlcyA9IEZBTFNFKXsNCiAgcmV0dXJuKGxpc3Qobl9jdiA9IG5fY3YsDQogICAgICAgICAgICAgIG1ldGhvZCA9IG1ldGhvZCwNCiAgICAgICAgICAgICAgb3B0X21ldGhvZHMgPSBvcHRfbWV0aG9kcywNCiAgICAgICAgICAgICAga2VybmVscyA9IGtlcm5lbHMsDQogICAgICAgICAgICAgIHNjYWxlX2ZlYXR1cmVzID0gc2NhbGVfZmVhdHVyZXMpKQ0KfQ0KYGBgDQoNCg0KPHNwYW4gc3R5bGU9ImNvbG9yOiAjMUZBQUUzOyI+PHU+RnVuY3Rpb248L3U+PC9zcGFuPjogYEtGQ3JlZ2ANCj09PQ0KDQpUaGlzIGZ1bmN0aW9uIGlzIHRoZSBjb21wbGV0ZSBpbXBsZW1lbnRhdGlvbiBvZiBLRkMgcHJvY2VkdXJlLg0KDQotICoqQXJndW1lbnQqKjoNCg0KICAgIC0gYHRyYWluX2lucHV0YCA6IHRoZSBtYXRyaXggb3IgZGF0YSBmcmFtZSBvZiB0cmFpbmluZyBpbnB1dCBkYXRhLg0KICAgIC0gYHRyYWluX3Jlc3BvbnNlYCA6IHRoZSB0cmFpbmluZyByZXNwb25zZSB2YXJpYWJsZS4NCiAgICAtIGB0ZXN0X2lucHV0YDogdGhlIHRlc3RpbmcgaW5wdXQgZGF0YS4NCiAgICAtIGB0ZXN0X3Jlc3BvbnNlYCA6IHRoZSByZXNwb25zZSB2YXJpYWJsZSBvZiB0aGUgdGVzdGluZyBkYXRhLiBJdCBpcyBvcHRpb25hbC4gSWYgaXQgaXMgbm90IGBOVUxMYCwgdGhlIG1lYW4gc3F1YXJlIGVycm9yIChtc2UpIGlzIGNvbXB1dGVkLg0KICAgIC0gYG5fY3ZgIDogdGhlIG51bWJlciBvZiBmb2xkcyBpbiBjcm9zcy12YWxpZGF0aW9uLg0KICAgIC0gYHBhcmFsbGVsYCA6IGEgbG9naWNhbCB2YWx1ZSBzcGVjaWZ5aW5nIHdoZXRoZXIgb3Igbm90IHRvIHBlcmZvcm0gJEskLW1lYW5zIGFsZ29yaXRobSAoc3RlcCAkSyQpIGluIHBhcmFsbGVsLiBCeSBkZWZhdWx0LCBgcGFyYWxsZWwgPSBUUlVFYC4gTm90ZSB0aGF0ICoqaW50ZXJuZXQgY29ubmVjdGlvbioqIGlzIHJlcXVpcmVkIGluIHRoaXMgY2FzZS4NCiAgICAtIGBpbnZfc2lnbWFgLCBgYWxwYCA6IHRoZSBpbnZlcnNlIG5vcm1hbGl6ZWQgY29uc3RhbnQgJFxzaWdtYV57LTF9PjAkIGFuZCB0aGUgZXhwb25lbnQgJFxhbHBoYT4wJCBvZiBleHBvbmVudGlhbCBrZXJuZWw6ICRLKHgpPWVeey1cfHgvXHNpZ21hXHxee1xhbHBoYX19JCBmb3IgYW55ICR4XGluXG1hdGhiYntSfV5kJC4gQnkgZGVmYXVsdCwgYGludl9zaWdtYSA9IGAkXHNxcnR7MS8yfSQgYW5kIGBhbHBoYSA9IDJgIHdoaWNoIGNvcnJlc3BvbmRzIHRvIHRoZSBHYXVzc2lhbiBrZXJuZWwuDQogICAgLSBgS19zdGVwYCA6IGFuIG9iamVjdCBvYnRhaW5lZCBmcm9tIGZ1bmN0aW9uIGBzdGVwS2AsIGFsbG93aW5nIHRvIHNldCB0aGUgdmFsdWVzIG9mIHRoZSBwYXJhbXRlcnMgaW4gc3RlcCAkSyQgb2YgdGhlIHByb2NlZHVyZS4NCiAgICAtIGBGX3N0ZXBgIDogYW4gb2JqZWN0IG9idGFpbmVkIGZyb20gZnVuY3Rpb24gYHN0ZXBGYCwgYWxsb3dpbmcgdG8gc2V0IHRoZSB2YWx1ZXMgb2YgdGhlIHBhcmFtdGVycyBpbiBzdGVwICRGJCBvZiB0aGUgcHJvY2VkdXJlLg0KICAgIC0gYENfc3RlcGAgOiBhbiBvYmplY3Qgb2J0YWluZWQgZnJvbSBmdW5jdGlvbiBgc3RlcENgLCBhbGxvd2luZyB0byBzZXQgdGhlIHZhbHVlcyBvZiB0aGUgcGFyYW10ZXJzIGluIHN0ZXAgJEMkIG9mIHRoZSBwcm9jZWR1cmUuDQogICAgLSBgc2V0R3JhZFBhcmFtQWdnYCA6IGFuIG9iamVjdCBmcm9tIHRoZSBgc2V0R3JhZFBhcmFtZXRlcmAgZnVuY3Rpb24sIGFsbG93aW5nIHRvIHNldCB0aGUgdmFsdWVzIG9mIHRoZSBwYXJhbWV0ZXJzIG9mICoqZ3JhZGllbnQgZGVzY2VudCoqIGFsZ29yaXRobSBmb3IgdGhlIDxzcGFuIHN0eWxlPSJjb2xvcjogI0U2MTgwQTsiPioqMXN0IGFnZ3JlZ2F0aW9uIG1ldGhvZCReMSQqKjwvc3Bhbj4uDQogICAgLSBgc2V0R3JpZFBhcmFtQWdnYCA6IGFuIG9iamVjdCBmcm9tIHRoZSBgc2V0R3JpZFBhcmFtZXRlcmAgZnVuY3Rpb24sIGFsbG93aW5nIHRvIHNldCB0aGUgdmFsdWVzIG9mIHRoZSBwYXJhbWV0ZXJzIG9mIHRoZSAqKmdyaWQgc2VhcmNoKiogYWxnb3JpdGhtIGZvciB0aGUgPHNwYW4gc3R5bGU9ImNvbG9yOiAjRTYxODBBOyI+Kioxc3QgYWdncmVnYXRpb24gbWV0aG9kJF4xJCoqPC9zcGFuPi4NCiAgICAtIGBzZXRHcmFkUGFyYW1NaXhgIDogYW4gb2JqZWN0IGZyb20gdGhlIGBzZXRHcmFkUGFyYW1ldGVyX01peGAgZnVuY3Rpb24sIGFsbG93aW5nIHRvIHNldCB0aGUgdmFsdWVzIG9mIHRoZSBwYXJhbWV0ZXJzIG9mIHRoZSAqKmdyYWRpZW50IGRlc2NlbnQqKiBhbGdvcml0aG0gZm9yIHRoZSA8c3BhbiBzdHlsZT0iY29sb3I6ICMwODMyQ0QiPioqMm5kIGFnZ3JlZ2F0aW9uIG1ldGhvZCReMiQqKjwvc3Bhbj4uDQogICAgLSBgc2V0R3JpZFBhcmFtTWl4YCA6IGFuIG9iamVjdCBmcm9tIHRoZSBgc2V0R3JpZFBhcmFtZXRlcl9NaXhgIGZ1bmN0aW9uLCBhbGxvd2luZyB0byBzZXQgdGhlIHZhbHVlcyBvZiB0aGUgcGFyYW1ldGVycyBvZiB0aGUgKipncmlkIHNlYXJjaCoqIGFsZ29yaXRobSBmb3IgdGhlIDxzcGFuIHN0eWxlPSJjb2xvcjogIzA4MzJDRCI+KioybmQgYWdncmVnYXRpb24gbWV0aG9kJF4yJCoqPC9zcGFuPi4NCiAgICAtIGBzaWxlbnRgIDogYSBsb2dpY2FsIHZhbHVlIHRvIHNpbGVudCBhbGwgdGhlIG1lc3NhZ2VzIGR1cmluZyB0aGUgYWxnb3JpdGhtLg0KICAgIA0KLSAqKlZhbHVlKio6DQoNCiAgICBUaGlzIGZ1bmN0aW9uIHJldHVybnMgYSBsaXN0IG9mIHRoZSBmb2xsb3dpbmcgb2JqZWN0czoNCiAgICANCiAgICAtIGBwcmVkaWN0X2ZpbmFsYCA6IHRoZSBmaW5hbCBwcmVkaWN0aW9ucyBnaXZlbiBieSB0aGUgYWdncmVnYXRpb24gb2YgYWxsIHRoZSBjYW5kaWRhdGUgbW9kZWxzLg0KICAgIC0gYHByZWRpY3RfbG9jYWxgIDogdGhlIHByZWRpY3Rpb25zIGdpdmVuIGJ5IGFsbCB0aGUgaW5kaXZpZHVhbCBjYW5kaWRhdGUgbW9kZWxzLg0KICAgIC0gYGFnZ19tZXRob2RgIDogdGhlIGxpc3Qgb2YgYWdncmVnYXRpb24gbWV0aG9kcyBvYnRhaW5lZCBpbiB0aGUgc3RlcCAkQyQgb2YgdGhlIHByb2NlZHVyZS4NCiAgICAtIGBydW5uaW5nX3RpbWVgIDogdGhlIGNvbXB1dGF0aW9uYWwgdGltZSBvZiB0aGUgYWxnb3JpdGhtLg0KDQotLS0tDQoNCiAgPiDwn6e+ICoqUmVtYXJrLjIqKjogVGhlIGBwYXJhbGxlbGAgYXJndW1lbnQgYWJvdmUgcmVxdWlyZXMgaW50ZXJuZXQgY29ubmVjdGlvbiB0byBsb2FkIHRoZSBzb3VyY2UgY29kZXMgb2YgJEskLW1lYW5zIGFsZ29yaXRobSB3aXRoIEJEcyBmcm9tIFtHaXRIdWIgPHNwYW4gc3R5bGU9ImNvbG9yOiAjMDk3QkMxIj4gYHIgZm9udGF3ZXNvbWU6OmZhKCJnaXRodWIiKWA8L3NwYW4+XShodHRwczovL2dpdGh1Yi5jb20vaGFzc290aGVhL0tGQy1Qcm9jZWR1cmUvYmxvYi9tYXN0ZXIva21lYW5CRC5SKS4gSXQgaXMgcGVyZm9ybWVkIG9uIHRoZSBtYXhpbXVtIG51bWJlciBvZiBjbHVzdGVycyBvZiB5b3VyIG1hY2hpbmUsIGFuZCB0aGUgc3BlZWQgaXMgYXQgbGVhc3QgdHdvIHRpbWVzIGZhc3RlciB0aGFuIHdpdGhvdXQgcGFyYWxsZWxpc20sIGhvd2V2ZXIsIGl0IGlzIG5vdCBzbyBzdGFibGUgZGVwZW5kaW5nIG9uIHlvdXIgaW50ZXJuZXQgY29ubmVjdGlvbiBvciBtYWNoaW5lLiANCg0KPiBGb3IgdGhlIGFnZ3JlZ2F0aW9uIG1ldGhvZHMgaW4gc3RlcCAkQyQ6DQoNCg0KLSA8c3BhbiBzdHlsZT0iY29sb3I6ICNFNjE4MEE7Ij4kXjEkPC9zcGFuPiBpcyB0aGUga2VybmVsLWJhc2VkIGNvbnNlbnN1YWwgYWdncmVnYXRpb24gZm9yIHJlZ3Jlc3Npb24gYnkgW0hhcyAoMjAyMSldKGh0dHBzOi8vaGFsLmFyY2hpdmVzLW91dmVydGVzLmZyL2hhbC0wMjg4NDMzM3Y1KS4gVGhlIHNvdXJjZSBjb2RlcyBvZiB0aGVzZSBmdW5jdGlvbnMgYXJlIGF2YWlsYWJsZSBpbiBbQWdncmVnYXRpb25NZXRob2RzXShodHRwczovL2dpdGh1Yi5jb20vaGFzc290aGVhL0FnZ3JlZ2F0aW9uTWV0aG9kcykgZ2l0aHViIHJlcG9zaXRvcnkgPHNwYW4gc3R5bGU9ImNvbG9yOiAjMDk3QkMxIj4gYHIgZm9udGF3ZXNvbWU6OmZhKCJnaXRodWIiKWA8L3NwYW4+IChmaWxlIG5hbWU6IFtLZXJuZWxBZ2dSZWcuUl0oaHR0cHM6Ly9naXRodWIuY29tL2hhc3NvdGhlYS9BZ2dyZWdhdGlvbk1ldGhvZHMvYmxvYi9tYWluL0tlcm5lbEFnZ1JlZy5SKSksIGFuZCB0aGUgZG9jdW1lbnRhdGlvbiBpcyBhdmFpbGFibGUgW2hlcmVdKGh0dHBzOi8vaGFzc290aGVhLmdpdGh1Yi5pby9maWxlcy9LZXJuZWxBZ2dSZWcvS2VybmVsQWdnUmVnLmh0bWwpLg0KLSA8c3BhbiBzdHlsZT0iY29sb3I6ICMwODMyQ0QiPiReMiQ8L3NwYW4+IGlzIHRoZSBhZ2dyZWdhdGlvbiB1c2luZyBpbnB1dC1vdXRwdXQgdHJhZGUtb2ZmIGJ5IFtGaXNjaGVyIGFuZCBNb3VnZW90ICgyMDE5KV0oaHR0cHM6Ly93d3cuc2NpZW5jZWRpcmVjdC5jb20vc2NpZW5jZS9hcnRpY2xlL3BpaS9TMDM3ODM3NTgxODMwMjM0OSkuIFRoZSBzb3VyY2UgY29kZXMgb2YgdGhlc2UgZnVuY3Rpb25zIGFyZSBhdmFpbGFibGUgaW4gW0FnZ3JlZ2F0aW9uTWV0aG9kc10oaHR0cHM6Ly9naXRodWIuY29tL2hhc3NvdGhlYS9BZ2dyZWdhdGlvbk1ldGhvZHMpIGdpdGh1YiByZXBvc2l0b3J5IDxzcGFuIHN0eWxlPSJjb2xvcjogIzA5N0JDMSI+IGByIGZvbnRhd2Vzb21lOjpmYSgiZ2l0aHViIilgPC9zcGFuPiAoZmlsZSBuYW1lOiBbTWl4Q29icmFSZWcuUl0oaHR0cHM6Ly9naXRodWIuY29tL2hhc3NvdGhlYS9BZ2dyZWdhdGlvbk1ldGhvZHMvYmxvYi9tYWluL01peENvYnJhUmVnLlIpKSwgYW5kIHRoZSBkb2N1bWVudGF0aW9uIGlzIGF2YWlsYWJsZSBvbiBbaGVyZV0oaHR0cHM6Ly9oYXNzb3RoZWEuZ2l0aHViLmlvL2ZpbGVzL0tlcm5lbEFnZ1JlZy9NaXhDb2JyYVJlZy5odG1sKS4NCg0KLS0tLQ0KDQoNCmBgYHtyfQ0KS0ZDcmVnID0gZnVuY3Rpb24odHJhaW5faW5wdXQsDQogICAgICAgICAgICAgICAgICB0cmFpbl9yZXNwb25zZSwNCiAgICAgICAgICAgICAgICAgIHRlc3RfaW5wdXQsDQogICAgICAgICAgICAgICAgICB0ZXN0X3Jlc3BvbnNlID0gTlVMTCwNCiAgICAgICAgICAgICAgICAgIG5fY3YgPSA1LA0KICAgICAgICAgICAgICAgICAgcGFyYWxsZWwgPSBUUlVFLA0KICAgICAgICAgICAgICAgICAgaW52X3NpZ21hID0gc3FydCguNSksDQogICAgICAgICAgICAgICAgICBhbHAgPSAyLA0KICAgICAgICAgICAgICAgICAgS19zdGVwID0gc3RlcEsoc3BsaXRzID0gLjUpLA0KICAgICAgICAgICAgICAgICAgRl9zdGVwID0gc3RlcEYoKSwNCiAgICAgICAgICAgICAgICAgIENfc3RlcCA9IHN0ZXBDKCksDQogICAgICAgICAgICAgICAgICBzZXRHcmFkUGFyYW1BZ2cgPSBzZXRHcmFkUGFyYW1ldGVyKCksDQogICAgICAgICAgICAgICAgICBzZXRHcmlkUGFyYW1BZ2cgPSBzZXRHcmlkUGFyYW1ldGVyKCksDQogICAgICAgICAgICAgICAgICBzZXRHcmFkUGFyYW1NaXggPSBzZXRHcmFkUGFyYW1ldGVyX01peCgpLA0KICAgICAgICAgICAgICAgICAgc2V0R3JpZFBhcmFtTWl4ID0gc2V0R3JpZFBhcmFtZXRlcl9NaXgoKSwNCiAgICAgICAgICAgICAgICAgIHNpbGVudCA9IEZBTFNFKXsNCiAgc3RhcnRfdGltZSA8LSBTeXMudGltZSgpDQogIGxvb2t1cF9kaXZfbmFtZXMgPC0gYygiZXVjbGlkZWFuIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAiZ2tsIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAibG9naXN0aWMiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICJpdGFrdXJhIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAicG9seW5vbWlhbCIpDQogIGRpdl8gPC0gS19zdGVwJGRpdg0KICAjIyMgSyBzdGVwOiBLbWVhbnMgY2x1c3RlcmluZyB3aXRoIEJEcw0KICBpZiAoaXMubnVsbChLX3N0ZXAkZGl2KSl7DQogICAgZGl2ZXJnZW5jZXMgPC0gbG9va3VwX2Rpdl9uYW1lcw0KICAgIHdhcm5pbmcoIk5vIGRpdmVyZ2VuY2UgcHJvdmlkZWQhIEFsbCBvZiB0aGVtIGFyZSB1c2VkISIpDQogIH0NCiAgZWxzZXsNCiAgICBkaXZlcmdlbmNlcyA8LSBLX3N0ZXAkZGl2ICU+JSANCiAgICAgIG1hcF9jaHIoLmYgPSB+IG1hdGNoLmFyZyhhcmcgPSAueCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2hvaWNlcyA9IGxvb2t1cF9kaXZfbmFtZXMpKQ0KICB9DQogIGRpdl9saXN0IDwtIGRpdmVyZ2VuY2VzICU+JSANCiAgICBtYXAoLmYgPSAoXCh4KSBpZih4ICE9ICJwb2x5bm9taWFsIikgcmV0dXJuKHgpIGVsc2UgcmV0dXJuKHJlcCgicG9seW5vbWlhbCIsIGxlbmd0aChLX3N0ZXAkZGVnKSkpKSkgJT4lDQogICAgdW5saXN0DQogIGRlZ19saXN0IDwtIHJlcChOQSwgbGVuZ3RoKGRpdl8pKQ0KICBkZWdfbGlzdFtkaXZfbGlzdCA9PSAicG9seW5vbWlhbCJdIDwtIEtfc3RlcCRkZWcNCiAgZGl2X25hbWVzIDwtIG1hcDJfY2hyKC54ID0gZGl2X2xpc3QsDQogICAgICAgICAgICAgICAgICAgICAgICAueSA9IGRlZ19saXN0LA0KICAgICAgICAgICAgICAgICAgICAgICAgLmYgPSAoXCh4LCB5KSBpZihpcy5uYSh5KSkgcmV0dXJuKHgpIGVsc2UgcmV0dXJuKHBhc3RlMCh4LHkpKSkpDQogICMjIyBTdGVwIEs6IEttZWFucyBjbHVzdGVyaW5nIHdpdGggQnJlZ21hbiBkaXZlcmdlbmNlcw0KICBkbSA8LSBkaW0odHJhaW5faW5wdXQpDQogIGlkX3NodWZmbGUgPC0gdmVjdG9yKGxlbmd0aCA9IGRtWzFdKQ0KICBuX3RyYWluIDwtIGZsb29yKEtfc3RlcCRzcGxpdHMgKiBkbVsxXSkNCiAgaWRfc2h1ZmZsZVtzYW1wbGUoZG1bMV0sIG5fdHJhaW4pXSA8LSBUUlVFDQogIGlmKHBhcmFsbGVsKXsNCiAgICBudW1Db3JlcyA8LSBwYXJhbGxlbDo6ZGV0ZWN0Q29yZXMoKQ0KICAgIGRvUGFyYWxsZWw6OnJlZ2lzdGVyRG9QYXJhbGxlbChudW1Db3JlcykgIyB1c2UgbXVsdGljb3JlLCBzZXQgdG8gdGhlIG51bWJlciBvZiBvdXIgY29yZXMNCiAgICBrbWVhbl8gPC0gZm9yZWFjaChpPTE6bGVuZ3RoKGRpdl9uYW1lcykpICVkb3BhciUgew0KICAgICAgZGV2dG9vbHM6OnNvdXJjZV91cmwoImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9oYXNzb3RoZWEvS0ZDLVByb2NlZHVyZS9tYXN0ZXIva21lYW5CRC5SIikNCiAgICAgIGttZWFuc0JEKHRyYWluX2lucHV0ID0gdHJhaW5faW5wdXQsDQogICAgICAgICAgICAgICBLID0gS19zdGVwJEssDQogICAgICAgICAgICAgICBkaXYgPSBkaXZfbGlzdFtpXSwNCiAgICAgICAgICAgICAgIG5fc3RhcnQgPSBLX3N0ZXAkbl9zdGFydCwNCiAgICAgICAgICAgICAgIG1heEl0ZXIgPSBLX3N0ZXAkbWF4SXRlciwNCiAgICAgICAgICAgICAgIGRlZyA9IGRlZ19saXN0W2ldLA0KICAgICAgICAgICAgICAgc2NhbGVfaW5wdXQgPSBLX3N0ZXAkc2NhbGVfaW5wdXQsDQogICAgICAgICAgICAgICBzcGxpdHMgPSBLX3N0ZXAkc3BsaXRzLA0KICAgICAgICAgICAgICAgZXBzaWxvbiA9IEtfc3RlcCRlcHNpbG9uLA0KICAgICAgICAgICAgICAgY2VudGVyXyA9IEtfc3RlcCRjZW50ZXJfLA0KICAgICAgICAgICAgICAgc2NhbGVfID0gS19zdGVwJHNjYWxlXywNCiAgICAgICAgICAgICAgIGlkX3NodWZmbGUgPSBpZF9zaHVmZmxlKQ0KICAgIH0NCiAgICBkb1BhcmFsbGVsOjpzdG9wSW1wbGljaXRDbHVzdGVyKCkNCiAgfSBlbHNlew0KICAgIGttZWFuXyA8LSBtYXAyKC54ID0gZGl2X2xpc3QsDQogICAgICAgICAgICAgICAgICAgLnkgPSBkZWdfbGlzdCwNCiAgICAgICAgICAgICAgICAgICAuZiA9IH4ga21lYW5zQkQodHJhaW5faW5wdXQgPSB0cmFpbl9pbnB1dCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgSyA9IEtfc3RlcCRLLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkaXYgPSAueCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbl9zdGFydCA9IEtfc3RlcCRuX3N0YXJ0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYXhJdGVyID0gS19zdGVwJG1heEl0ZXIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRlZyA9IC55LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IEtfc3RlcCRzY2FsZV9pbnB1dCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3BsaXRzID0gS19zdGVwJHNwbGl0cywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZXBzaWxvbiA9IEtfc3RlcCRlcHNpbG9uLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjZW50ZXJfID0gS19zdGVwJGNlbnRlcl8sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlXyA9IEtfc3RlcCRzY2FsZV8sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGlkX3NodWZmbGUgPSBpZF9zaHVmZmxlKSkNCiAgfQ0KICBuYW1lcyhrbWVhbl8pIDwtIGRpdl9uYW1lcw0KICAjIyMgRiBzdGVwOiBGaXR0aW5nIHRoZSBjb3JyZXNwb25kaW5nIG1vZGVsIG9uIGVhY2ggb2JzZXJ2ZWQgY2x1c3Rlcg0KICBtb2RlbF8gPC0gZGl2X25hbWVzICU+JQ0KICAgIG1hcCguZiA9IH4gZml0TG9jYWxNb2RlbHMoa21lYW5fW1sueF1dLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJhaW5fcmVzcG9uc2UgPSB0cmFpbl9yZXNwb25zZSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1vZGVsID0gRl9zdGVwJG1vZGVsLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZm9ybXVsYSA9IEZfc3RlcCRmb3JtdWxhKSkNCiAgbmFtZXMobW9kZWxfKSA8LSBkaXZfbmFtZXMNCiAgcHJlZF9jb21iaW5lIDwtIG1vZGVsXyAlPiUNCiAgICBtYXBfZGZjKC5mID0gfiAueCRkYXRhX3JlbWFpbiRmaXQpDQogIHlfcmVtYWluIDwtIHRyYWluX3Jlc3BvbnNlWyFpZF9zaHVmZmxlXQ0KICBwcmVkX3Rlc3QgPC0gZGl2X25hbWVzICU+JQ0KICAgIG1hcF9kZmMoLmYgPSB+IGxvY2FsUHJlZGljdChtb2RlbF9bWy54XV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3RfaW5wdXQpKQ0KICBuYW1lcyhwcmVkX3Rlc3QpIDwtIG5hbWVzKHByZWRfY29tYmluZSkgPC0gZGl2X25hbWVzDQogICMgQyBzdGVwOiBDb25zZW5zdWFsIHJlZ3Jlc3Npb24gYWdncmVnYXRpb24gbWV0aG9kIHdpdGgga2VybmVsLWJhc2VkIENPQlJBDQogIGxpc3RfbWV0aG9kX2FnZyA8LSBsaXN0KG1peGNvYnJhID0gZnVuY3Rpb24ocHJlZCl7TWl4Q29icmFSZWcodHJhaW5faW5wdXQgPSB0cmFpbl9pbnB1dFshaWRfc2h1ZmZsZSxdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluX3Jlc3BvbnNlID0geV9yZW1haW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdF9pbnB1dCA9IHRlc3RfaW5wdXQsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJhaW5fcHJlZGljdGlvbnMgPSBwcmVkLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3RfcHJlZGljdGlvbnMgPSBwcmVkX3Rlc3QsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdF9yZXNwb25zZSA9IHRlc3RfcmVzcG9uc2UsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfaW5wdXQgPSBLX3N0ZXAkc2NhbGVfaW5wdXQsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfbWFjaGluZSA9IENfc3RlcCRzY2FsZV9mZWF0dXJlcywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX2N2ID0gQ19zdGVwJG5fY3YsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW52X3NpZ21hID0gaW52X3NpZ21hLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFscCA9IGFscCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrZXJuZWxzID0gQ19zdGVwJGtlcm5lbHMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3B0aW1pemVNZXRob2QgPSBDX3N0ZXAkb3B0X21ldGhvZHMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2V0R3JhZFBhcmFtID0gc2V0R3JhZFBhcmFtTWl4LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNldEdyaWRQYXJhbSA9IHNldEdyaWRQYXJhbU1peCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaWxlbnQgPSBzaWxlbnQpfSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgY29icmEgPSBmdW5jdGlvbihwcmVkKXtrZXJuZWxBZ2dSZWcodHJhaW5fZGVzaWduID0gcHJlZCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJhaW5fcmVzcG9uc2UgPSB5X3JlbWFpbiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGVzdF9kZXNpZ24gPSBwcmVkX3Rlc3QsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3RfcmVzcG9uc2UgPSB0ZXN0X3Jlc3BvbnNlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IEtfc3RlcCRzY2FsZV9pbnB1dCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfbWFjaGluZSA9IENfc3RlcCRzY2FsZV9mZWF0dXJlcywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYnVpbGRfbWFjaGluZSA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtYWNoaW5lcyA9IE5VTEwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fY3YgPSBDX3N0ZXAkbl9jdiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaW52X3NpZ21hID0gc3FydCguNSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFscCA9IDIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGtlcm5lbHMgPSBDX3N0ZXAka2VybmVscywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3B0aW1pemVNZXRob2QgPSBDX3N0ZXAkb3B0X21ldGhvZHMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNldEdyYWRQYXJhbSA9IHNldEdyYWRQYXJhbUFnZywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2V0R3JpZFBhcmFtID0gc2V0R3JpZFBhcmFtQWdnLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaWxlbnQgPSBzaWxlbnQpfSkNCiAgcmVzIDwtIG1hcCgueCA9IENfc3RlcCRtZXRob2QsDQogICAgICAgICAgICAgLmYgPSB+IGxpc3RfbWV0aG9kX2FnZ1tbLnhdXShwcmVkX2NvbWJpbmUpKQ0KICBsaXN0X2FnZ19tZXRob2RzIDwtIGxpc3QoY29icmEgPSAiY29iIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG1peGNvYnJhID0gIm1peCIpDQogIG5hbWVzKHJlcykgPC0gQ19zdGVwJG1ldGhvZA0KICBleHRfZnVuIDwtIGZ1bmN0aW9uKEwsIG5hbSl7DQogICAgdGFiIDwtIEwkZml0dGVkX2FnZ3JlZ2F0ZQ0KICAgIG5hbWVzKHRhYikgPC0gcGFzdGUwKG5hbWVzKHRhYiksICJfIiwgbmFtKQ0KICAgIHJldHVybih0YWIpDQogIH0NCiAgcHJlZF9maW4gPC0gQ19zdGVwJG1ldGhvZCAlPiUNCiAgICBtYXBfZGZjKC5mID0gfiBleHRfZnVuKHJlc1tbLnhdXSwgbGlzdF9hZ2dfbWV0aG9kc1tbLnhdXSkpDQogIHRpbWUudGFrZW4gPC0gU3lzLnRpbWUoKSAtIHN0YXJ0X3RpbWUNCiAgIyMjIFRvIGZpbmlzaA0KICBpZihpcy5udWxsKHRlc3RfcmVzcG9uc2UpKXsNCiAgICByZXR1cm4obGlzdCgNCiAgICBwcmVkaWN0X2ZpbmFsID0gcHJlZF9maW4sDQogICAgcHJlZGljdF9sb2NhbCA9IHByZWRfdGVzdCwNCiAgICBhZ2dfbWV0aG9kID0gcmVzLA0KICAgIHJ1bm5pbmdfdGltZSA9IHRpbWUudGFrZW4NCiAgKSkNCiAgfSBlbHNlew0KICAgIGVycm9yIDwtIGNiaW5kKHByZWRfdGVzdCwgcHJlZF9maW4pICU+JQ0KICAgICAgZHBseXI6Om11dGF0ZSh5X3Rlc3QgPSB0ZXN0X3Jlc3BvbnNlKSAlPiUNCiAgICAgIGRwbHlyOjpzdW1tYXJpc2VfYWxsKC5mdW5zID0gfiAoLiAtIHlfdGVzdCkpICU+JQ0KICAgICAgZHBseXI6OnNlbGVjdCgteV90ZXN0KSAlPiUNCiAgICAgIGRwbHlyOjpzdW1tYXJpc2VfYWxsKC5mdW5zID0gfiBtZWFuKC5eMikpDQogICAgcmV0dXJuKGxpc3QoDQogICAgICBwcmVkaWN0X2ZpbmFsID0gcHJlZF9maW4sDQogICAgICBwcmVkaWN0X2xvY2FsID0gcHJlZF90ZXN0LA0KICAgICAgYWdnX21ldGhvZCA9IHJlcywNCiAgICAgIG1zZSA9IGVycm9yLA0KICAgICAgcnVubmluZ190aW1lID0gdGltZS50YWtlbg0KICApKQ0KICB9DQp9DQpgYGANCg0KPiAqKkV4YW1wbGUuNCoqOiBBIGNvbXBsZXRlIEtGQyBwcm9jZWR1cmUgaXMgaW1wbGVtZW50ZWQgb24gdGhlIHNhbWUgQWJhbG9uZSBkYXRhLCB1c2luZyAkNSQgQkRzIGAiZXVjbGlkZWFuImAsIGAiaXRha3VyYSJgLCBgImdrbCJgIGFuZCBgInBvbHlub21pYWwiYCAob2YgZGVncmVlICQzJCBhbmQgJDYkKS4gQm90aCBhZ2dyZWdhdGlvbiBtZXRob2RzIGFyZSB1c2VkIGluIHRoZSBzdGVwICRDJC4gVHdvIGtlcm5lbCBmdW5jdGlvbnMgYXJlIHVzZWQgZm9yIGVhY2ggYWdncmVnYXRpb24gbWV0aG9kOiBgImdhdXNzaWFuImAgKHdpdGggZ3JhZGllbnQgZGVzY2VudCBhbGdvcml0aG0pIGFuZCBgImVwYW5lY2huaWtvdiJgICh3aXRoIGdyaWQgc2VhcmNoIGFsZ29yaXRobSkuDQoNCmBgYHtyfQ0KdHJhaW4xIDwtIGxvZ2ljYWwobikNCnRyYWluMVtzYW1wbGUobiwgIGZsb29yKG4qMC44KSldIDwtIFRSVUUNCmtmYzEgPC0gS0ZDcmVnKHRyYWluX2lucHV0ID0gZGZbdHJhaW4xLDI6bmNvbChkZildLA0KICAgICAgICAgICAgICAgIHRyYWluX3Jlc3BvbnNlID0gZGYkUmluZ3NbdHJhaW4xXSwNCiAgICAgICAgICAgICAgICB0ZXN0X2lucHV0ID0gZGZbIXRyYWluMSwyOm5jb2woZGYpXSwNCiAgICAgICAgICAgICAgICBLX3N0ZXAgPSBzdGVwSyhLID0gMywNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzY2FsZV9pbnB1dCA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGl2ID0gYygiZXVjbCIsICJpdGEiLCAiZ2tsIiwgInBvbHkiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkZWcgPSBjKDMsIDYpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNwbGl0cyA9IC41KSwNCiAgICAgICAgICAgICAgICBDX3N0ZXAgPSBzdGVwQyhtZXRob2QgPSBjKCJjb2JyYSIsICJtaXhjb2JyYSIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9wdF9tZXRob2RzID0gYygiZ3JhZCIsICJncmlkIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAga2VybmVscyA9IGMoImdhdXNzaWFuIiwgImdhdXNzaWFuIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGVfZmVhdHVyZXMgPSBGQUxTRSksDQogICAgICAgICAgICAgICAgc2V0R3JhZFBhcmFtQWdnID0gc2V0R3JhZFBhcmFtZXRlcihyYXRlID0gMC4yKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICNjb2VmX2xtID0gMiksDQogICAgICAgICAgICAgICAgc2V0R3JpZFBhcmFtQWdnID0gc2V0R3JpZFBhcmFtZXRlcihtaW5fdmFsID0gLjAwMDAxLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4X3ZhbCA9IDEwLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbl92YWwgPSAxMDApLA0KICAgICAgICAgICAgICAgIHNldEdyYWRQYXJhbU1peCA9IHNldEdyYWRQYXJhbWV0ZXJfTWl4KHJhdGUgPSAibGluZWFyIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjb2VmX2F1dG8gPSBjKC41LC41KSksDQogICAgICAgICAgICAgICAgc2V0R3JpZFBhcmFtTWl4ID0gc2V0R3JpZFBhcmFtZXRlcl9NaXgobWluX2FscGhhID0gMWUtMTAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4X2FscGhhID0gMC41LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1pbl9iZXRhID0gMWUtMTAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWF4X2JldGEgPSAxLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5fYWxwaGEgPSAyMCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX2JldGEgPSAyMCkpDQpgYGANCg0KPiBUaGUgbWVhbiBzcXVhcmUgZXJyb3JzIGV2YWx1YXRlZCBvbiAkMjBcJSQtdGVzdGluZyBkYXRhIG9mIHRoZSBhYm92ZSBjb21wdXRhdGlvbiBhcmUgcmVwb3J0ZWQgYmVsb3cuDQoNCmBgYHtyfQ0KcmYxIDwtIHJhbmRvbUZvcmVzdDo6cmFuZG9tRm9yZXN0KFJpbmdzIH4gLiwgZGF0YSA9IGRmW3RyYWluMSwyOm5jb2woZGYpXSkNCmtmYzEkcHJlZGljdF9maW5hbCAlPiUNCiAgbXV0YXRlKHJmID0gcHJlZGljdChyZjEsIG5ld2RhdGEgPSBkZlshdHJhaW4xLDI6bmNvbChkZildKSkgJT4lDQogIHN3ZWVwKE1BUkdJTiA9IDEsIFNUQVRTID0gZGYkUmluZ3NbIXRyYWluMV0sIEZVTiA9ICItIikgJT4lDQogIC5eMiAgJT4lDQogIGNvbE1lYW5zDQpgYGANCg0KPiBXZSBjYW4gc2VlIHRoYXQgS0ZDIHByb2NlZHVyZSBwZXJmb3JtcyByZWFsbHkgd2VsbCBvbiB0aGlzIHJlYWwtbGlmZSBkYXRhc2V0Lg0KDQotIFtIYXMgZXQgYWwuICgyMDIxKV0oaHR0cHM6Ly93d3cudGFuZGZvbmxpbmUuY29tL2VwcmludC9ZS0dTOEdUS0RCS1lGWEVHRldTQi9mdWxsP3RhcmdldD0xMC4xMDgwLzAwOTQ5NjU1LjIwMjEuMTg5MTUzOSkNCi0gW0Zpc2NoZXIgYW5kIE1vdWdlb3QgKDIwMTkpXShodHRwczovL3d3dy5zY2llbmNlZGlyZWN0LmNvbS9zY2llbmNlL2FydGljbGUvcGlpL1MwMzc4Mzc1ODE4MzAyMzQ5KQ0KLSBbSGFzICgyMDIxKV0oaHR0cHM6Ly9oYWwuYXJjaGl2ZXMtb3V2ZXJ0ZXMuZnIvaGFsLTAyODg0MzMzdjUpDQotIFtCaWF1IGV0IGFsLiAoMjAxNildKGh0dHBzOi8vd3d3LnNjaWVuY2VkaXJlY3QuY29tL3NjaWVuY2UvYXJ0aWNsZS9waWkvUzAwNDcyNTlYMTUwMDA5NTApDQotIC4uLg0KLSBbZHBseXIgdmlkZW9zXShodHRwczovL3d3dy55b3V0dWJlLmNvbS9oYXNodGFnL2RwbHlyKSBgciBmb250YXdlc29tZTo6ZmEoInZpZGVvIilgDQotIFtnZ3Bsb3QyIHZpZGVvIHR1dG9yaWFsXShodHRwczovL3d3dy55b3V0dWJlLmNvbS9oYXNodGFnL2dncGxvdDIpIGByIGZvbnRhd2Vzb21lOjpmYSgidmlkZW8iKWANCi0gW1IgZm9yIGRhdGEgc2NpZW5jZV0oaHR0cHM6Ly9yNGRzLmhhZC5jby5uei8pDQoNCi0tLQ0KDQo+IDxzcGFuIHN0eWxlPSJjb2xvcjogIzFGQUFFMzsiPiYjMTI4MjE0OyBSZWFkIGFsc28gW0tlcm5lbEFnZ1JlZ10oaHR0cHM6Ly9oYXNzb3RoZWEuZ2l0aHViLmlvL2ZpbGVzL0NvZGVzUGhEL0tlcm5lbEFnZ1JlZy5odG1sKSBhbmQgW01peENvYnJhUmVnXShodHRwczovL2hhc3NvdGhlYS5naXRodWIuaW8vZmlsZXMvQ29kZXNQaEQvTWl4Q29icmFSZWcuaHRtbCk8L3NwYW4+Lg0KDQotLS0=