Purpose

  • The model-agnostic Shapley value approximation algorithm implemented in ShapML is computationally expensive. Because of this, the goal of this vignette is to (a) give a sense of the real-world clock time needed to explain datasets of various sizes and (b) compare these times to those from other popular open source packages implementing similar algorithms.

Setup

  • Instances: 1, 100, 1000
  • Features: 10, 50, 200
  • Comparison packages:
    • fastshap v0.0.5 in R with a C++ back end
  • System
    • Windows
    • Non-parallel computation
    • i7 8 GB laptop

Results

  • For all of the simulations examined, the Shapley value approximation algorithm in Julia’s ShapML was consistently and noticeably faster than the R implementation in fastshap.

  • These results may or may not hold for big data and parallel computation so check back for additional simulations.



Simulations

Load Packages

R

library(dplyr)
library(fastshap)
library(ggplot2)
library(JuliaCall)
library(lubridate)
library(ranger)
library(tidyr)
JuliaCall::julia_setup()

Sys.setenv(PATH = paste("C:/Users/nickr/AppData/Local/Julia-1.3.1/bin", Sys.getenv("PATH"), sep = ";"))

Julia

using DataFrames
using Random
using RCall
using ShapML

Simulation Design in R

n_instances <- c(1, 100, 1000)
n_features <- c(10, 50, 200)

conditions <- expand.grid("n_features" = n_features, "n_instances" = n_instances)
conditions$condition <- 1:nrow(conditions)

n_conditions <- nrow(conditions)
n_models <- length(n_features)
n_simulations <- 3
n_monte_carlo <- 20
seed <- 1

conditions

Train ML Models in R

  • Create a series of synthetic datasets, one for each set of features–10, 50, 200. We’ll down-sample each dataset to create new datasets with fewer instances for the various conditions in the simulation.

  • Train one Random Forest model for each set of features–10, 50, 200–with 1,000 instances.

data_train <- vector("list", n_models)
models <- vector("list", n_models)

for(i in 1:n_models) {
  data_train[[i]] <- fastshap::gen_friedman(max(n_instances), n_features[i], seed = seed)
  models[[i]] <- ranger::ranger(y ~ ., data = data_train[[i]], seed = seed)
}

names(data_train) <- n_features
names(models) <- n_features

Duplicate Datasets & Models in R

  • Duplicate the 3 model datasets and models across 9 simulation conditions with the appropriate number of instances and features.
data_explain <- vector("list", n_conditions)
models_condition <- vector("list", n_conditions)

for(i in 1:n_conditions) {
  
  data_condition <- data_train[[which(names(data_train) == as.character(conditions$n_features[i]))]]
  
  # 1 dataset of features to be explained for each condition.
  data_explain[[i]] <- data_condition[1:conditions$n_instances[i], !names(data_condition) %in% "y"]
  
  # 1 model for each condition.
  models_condition[[i]] <- models[[which(names(models) == as.character(conditions$n_features[i]))]]
}

Predict Wrapper Functions in R

  • The predict() functions will be written in R and then converted to Julia to work with the trained Random Forest model–which is not explicitly converted.
predict_fun_r <- function(object, newdata) {
  predict(object, data = newdata)$predictions
}

predict_fun_julia <- function(model, data) {
  data_pred = data.frame("y_pred" = predict(model, data)$predictions)  # Must return a DataFrame.
  return(data_pred)
}

Shapley Value Calculations

R

  • Explain model predictions with Shapley values using fastshap.
data_shap_r <- vector("list", n_conditions)
data_shap_r <- lapply(data_shap_r, function(pass){vector("list", n_simulations)})
runtime_r <- vector("list", n_conditions)
runtime_r <- lapply(data_shap_r, function(pass){vector("list", n_simulations)})

for (i in 1:n_conditions) {
  for (j in 1:n_simulations) {
    
      set.seed(seed)
      start_time <- Sys.time()
      
      data_shap_r[[i]][[j]] <- fastshap::explain(models_condition[[i]], 
                                                 X = data_explain[[i]],
                                                 pred_wrapper = predict_fun_r,
                                                 nsim = n_monte_carlo
                                                 )
      
      stop_time <- Sys.time()
      
      runtime_r[[i]][[j]] <- data.frame("time" = as.numeric(difftime(stop_time, start_time, units = "secs")),
                                        "condition" = i,
                                        "simulation" = j)
      
      print(runtime_r[[i]][[j]])
  }
}

runtime_r <- dplyr::bind_rows(unlist(runtime_r, recursive = FALSE))

Julia

  • Load R objects into the Julia environment.
n_conditions = RCall.reval("n_conditions")
n_conditions = convert(Integer, n_conditions)

n_simulations = RCall.reval("n_simulations")
n_simulations = convert(Integer, n_simulations)

models_condition = RCall.reval("models_condition")

predict_fun_julia = RCall.reval("predict_fun_julia")
predict_fun_julia = convert(Function, predict_fun_julia)

data_explain = RCall.reval("data_explain")

n_monte_carlo = RCall.reval("n_monte_carlo")
n_monte_carlo = convert(Integer, n_monte_carlo)

seed = RCall.reval("seed")
seed = convert(Integer, seed)
  • Explain model predictions with Shapley values using ShapML.
data_shap_julia = [Array{DataFrame}(undef, n_simulations) for i in 1:n_conditions]
runtime_julia = [Array{Any}(undef, n_simulations) for i in 1:n_conditions]

for i in 1:n_conditions
  for j in 1:n_simulations

      data_explain_temp = convert(DataFrame, data_explain[i])
      
      start_time = time()
      
      data_shap_julia[i][j] = ShapML.shap(explain = data_explain_temp,
                                          reference = data_explain_temp,
                                          model = models_condition[i],
                                          predict_function = predict_fun_julia,
                                          sample_size = n_monte_carlo,
                                          seed = seed
                                          )
      
      stop_time = time()
      
      runtime_julia[i][j] = DataFrame(time = stop_time - start_time, condition = i, simulation = j)
  end
end

runtime_julia = vcat(vcat(runtime_julia...)...)

Simulation Results

runtime_r$language <- "R"
runtime_julia$language <- "Julia"

data_runtime <- dplyr::bind_rows(runtime_r, runtime_julia)

data_runtime <- dplyr::left_join(data_runtime, conditions, by = "condition")
  • Plots:
    • Top: A comparison of runtimes as sample sizes increase.
    • Bottom: A comparison of runtimes as the number of features increases.

  • Results:
    • For all of the simulations examined, the Shapley value approximation algorithm in Julia’s ShapML is consistently and noticeably faster than the R implementation in fastshap.
plot_theme <- theme(
  strip.text = element_text(size = 12, face = "bold"),
  plot.title = element_text(size = 14, face = "bold"),
  plot.subtitle = element_text(size = 14),
  axis.title = element_text(size = 12, face = "bold"),
  axis.text.x = element_text(size = 12, face = "bold"),
  axis.text.y = element_text(size = 12, face = "bold"),
  legend.text = element_text(size = 12, face = "bold"),
  legend.position = "bottom"
)

p <- ggplot(data_runtime, aes(n_instances, time, color = language))
p <- p + geom_point(size = 5, alpha = .5)
p <- p + stat_smooth(method = "lm", se = FALSE, show.legend = FALSE, size = 1.1, alpha = .5)
p <- p + scale_color_manual(values = c("R" = "#276DC2", "Julia" = "#9558B2"))
p <- p + facet_wrap(~ n_features, scales = "free")
p <- p + theme_bw() + plot_theme
p <- p + xlab("Number of instances in dataset") + ylab("Runtime in seconds") + labs(color = NULL) +
  ggtitle("Shapley Approximation Algorithm Runtime - Julia vs R (Non-Parallel)",
          subtitle = "Faceted by number of features")
p_1 <- p
p_1


p <- ggplot(data_runtime, aes(n_features, time, color = language))
p <- p + geom_point(size = 5, alpha = .5)
p <- p + stat_smooth(method = "lm", formula = y ~ poly(x, 2), 
                     se = FALSE, show.legend = FALSE, size = 1.1, alpha = .5)
p <- p + scale_color_manual(values = c("R" = "#276DC2", "Julia" = "#9558B2"))
p <- p + scale_x_continuous(limits = c(0, 200))
p <- p + facet_wrap(~ n_instances, scales = "free")
p <- p + theme_bw() + plot_theme
p <- p + xlab("Number of features in dataset") + ylab("Runtime in seconds") + labs(color = NULL) +
  ggtitle("Shapley Approximation Algorithm Runtime - Julia vs R (Non-Parallel)",
          subtitle = "Faceted by number of instances")
p_2 <- p
p_2

LS0tDQp0aXRsZTogIioqU2hhcGxleSBTcGVlZCAtIEp1bGlhIHZzIHRoZSBSZXN0KioiDQpkYXRlOiAnYHIgU3lzLkRhdGUoKWAnDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiB0cnVlDQogICAgdG9jX2Zsb2F0OiB0cnVlDQotLS0NCg0KKioqDQoNCiMgKipQdXJwb3NlKioNCg0KKiBUaGUgbW9kZWwtYWdub3N0aWMgU2hhcGxleSB2YWx1ZSBhcHByb3hpbWF0aW9uIGFsZ29yaXRobSBpbXBsZW1lbnRlZCBpbiBgU2hhcE1MYCBpcyANCmNvbXB1dGF0aW9uYWxseSBleHBlbnNpdmUuIEJlY2F1c2Ugb2YgdGhpcywgdGhlIGdvYWwgb2YgdGhpcyB2aWduZXR0ZSBpcyB0byAoYSkgZ2l2ZSBhIHNlbnNlIA0Kb2YgdGhlIHJlYWwtd29ybGQgY2xvY2sgdGltZSBuZWVkZWQgdG8gZXhwbGFpbiBkYXRhc2V0cyBvZiB2YXJpb3VzIHNpemVzIGFuZCAoYikgY29tcGFyZSANCnRoZXNlIHRpbWVzIHRvIHRob3NlIGZyb20gb3RoZXIgcG9wdWxhciBvcGVuIHNvdXJjZSBwYWNrYWdlcyBpbXBsZW1lbnRpbmcgc2ltaWxhciBhbGdvcml0aG1zLg0KDQojICoqU2V0dXAqKg0KDQoqICoqSW5zdGFuY2VzOioqIDEsIDEwMCwgMTAwMA0KKiAqKkZlYXR1cmVzOioqIDEwLCA1MCwgMjAwDQoqICoqQ29tcGFyaXNvbiBwYWNrYWdlczoqKg0KICArICoqW2Zhc3RzaGFwXShodHRwczovL2dpdGh1Yi5jb20vYmdyZWVud2VsbC9mYXN0c2hhcCkqKiB2MC4wLjUgaW4gYFJgIHdpdGggYSBgQysrYCBiYWNrIGVuZA0KKiAqKlN5c3RlbSoqDQogICsgV2luZG93cw0KICArIE5vbi1wYXJhbGxlbCBjb21wdXRhdGlvbg0KICArIGk3IDggR0IgbGFwdG9wDQogIA0KIyAqKlJlc3VsdHMqKg0KDQoqIEZvciBhbGwgb2YgdGhlIHNpbXVsYXRpb25zIGV4YW1pbmVkLCB0aGUgU2hhcGxleSB2YWx1ZSBhcHByb3hpbWF0aW9uIGFsZ29yaXRobSBpbiBgSnVsaWFgJ3MgYFNoYXBNTGAgDQp3YXMgY29uc2lzdGVudGx5IGFuZCBub3RpY2VhYmx5IGZhc3RlciB0aGFuIHRoZSBgUmAgaW1wbGVtZW50YXRpb24gaW4gYGZhc3RzaGFwYC4NCg0KKiBUaGVzZSByZXN1bHRzIG1heSBvciBtYXkgbm90IGhvbGQgZm9yIGJpZyBkYXRhIGFuZCBwYXJhbGxlbCBjb21wdXRhdGlvbiBzbyBjaGVjayBiYWNrIGZvciBhZGRpdGlvbmFsIHNpbXVsYXRpb25zLg0KDQo8YnI+DQoNCiFbXSguL3NoYXBNTF9mYXN0c2hhcF9zaW1fcGxvdF8xX3N1bW1hcnkucG5nKQ0KDQoqKioNCg0KIyAqKlNpbXVsYXRpb25zKioNCg0KIyMgKipMb2FkIFBhY2thZ2VzKioNCg0KIyMjICoqYFJgKioNCg0KYGBge3IsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFfQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoZmFzdHNoYXApDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KEp1bGlhQ2FsbCkNCmxpYnJhcnkobHVicmlkYXRlKQ0KbGlicmFyeShyYW5nZXIpDQpsaWJyYXJ5KHRpZHlyKQ0KYGBgDQoNCg0KYGBge3IsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFfQ0KSnVsaWFDYWxsOjpqdWxpYV9zZXR1cCgpDQoNClN5cy5zZXRlbnYoUEFUSCA9IHBhc3RlKCJDOi9Vc2Vycy9uaWNrci9BcHBEYXRhL0xvY2FsL0p1bGlhLTEuMy4xL2JpbiIsIFN5cy5nZXRlbnYoIlBBVEgiKSwgc2VwID0gIjsiKSkNCmBgYA0KDQoNCiMjIyAqKmBKdWxpYWAqKg0KDQpgYGB7anVsaWF9DQp1c2luZyBEYXRhRnJhbWVzDQp1c2luZyBSYW5kb20NCnVzaW5nIFJDYWxsDQp1c2luZyBTaGFwTUwNCmBgYA0KDQoNCiMjICoqU2ltdWxhdGlvbiBEZXNpZ24gaW4gYFJgKioNCg0KYGBge3J9DQpuX2luc3RhbmNlcyA8LSBjKDEsIDEwMCwgMTAwMCkNCm5fZmVhdHVyZXMgPC0gYygxMCwgNTAsIDIwMCkNCg0KY29uZGl0aW9ucyA8LSBleHBhbmQuZ3JpZCgibl9mZWF0dXJlcyIgPSBuX2ZlYXR1cmVzLCAibl9pbnN0YW5jZXMiID0gbl9pbnN0YW5jZXMpDQpjb25kaXRpb25zJGNvbmRpdGlvbiA8LSAxOm5yb3coY29uZGl0aW9ucykNCg0Kbl9jb25kaXRpb25zIDwtIG5yb3coY29uZGl0aW9ucykNCm5fbW9kZWxzIDwtIGxlbmd0aChuX2ZlYXR1cmVzKQ0Kbl9zaW11bGF0aW9ucyA8LSAzDQpuX21vbnRlX2NhcmxvIDwtIDIwDQpzZWVkIDwtIDENCg0KY29uZGl0aW9ucw0KYGBgDQoNCiMjICoqVHJhaW4gTUwgTW9kZWxzIGluIGBSYCoqDQoNCiogQ3JlYXRlIGEgc2VyaWVzIG9mIHN5bnRoZXRpYyBkYXRhc2V0cywgb25lIGZvciBlYWNoIHNldCBvZiBmZWF0dXJlcy0tYHIgcGFzdGUobl9mZWF0dXJlcywgY29sbGFwc2UgPSAiLCAiKWAuIA0KV2UnbGwgZG93bi1zYW1wbGUgZWFjaCBkYXRhc2V0IHRvIGNyZWF0ZSBuZXcgZGF0YXNldHMgd2l0aCBmZXdlciBpbnN0YW5jZXMgZm9yIHRoZSB2YXJpb3VzIGNvbmRpdGlvbnMgaW4gdGhlIHNpbXVsYXRpb24uDQoNCiogVHJhaW4gb25lIFJhbmRvbSBGb3Jlc3QgbW9kZWwgZm9yIGVhY2ggc2V0IG9mIGZlYXR1cmVzLS1gciBwYXN0ZShuX2ZlYXR1cmVzLCBjb2xsYXBzZSA9ICIsICIpYC0td2l0aCAxLDAwMCBpbnN0YW5jZXMuDQoNCmBgYHtyfQ0KZGF0YV90cmFpbiA8LSB2ZWN0b3IoImxpc3QiLCBuX21vZGVscykNCm1vZGVscyA8LSB2ZWN0b3IoImxpc3QiLCBuX21vZGVscykNCg0KZm9yKGkgaW4gMTpuX21vZGVscykgew0KICBkYXRhX3RyYWluW1tpXV0gPC0gZmFzdHNoYXA6Omdlbl9mcmllZG1hbihtYXgobl9pbnN0YW5jZXMpLCBuX2ZlYXR1cmVzW2ldLCBzZWVkID0gc2VlZCkNCiAgbW9kZWxzW1tpXV0gPC0gcmFuZ2VyOjpyYW5nZXIoeSB+IC4sIGRhdGEgPSBkYXRhX3RyYWluW1tpXV0sIHNlZWQgPSBzZWVkKQ0KfQ0KDQpuYW1lcyhkYXRhX3RyYWluKSA8LSBuX2ZlYXR1cmVzDQpuYW1lcyhtb2RlbHMpIDwtIG5fZmVhdHVyZXMNCmBgYA0KDQoNCiMjICoqRHVwbGljYXRlIERhdGFzZXRzICYgTW9kZWxzIGluIGBSYCoqDQoNCiogRHVwbGljYXRlIHRoZSBgciBuX21vZGVsc2AgbW9kZWwgZGF0YXNldHMgYW5kIG1vZGVscyBhY3Jvc3MgYHIgbnJvdyhjb25kaXRpb25zKWAgc2ltdWxhdGlvbiBjb25kaXRpb25zIHdpdGggdGhlIGFwcHJvcHJpYXRlIA0KbnVtYmVyIG9mIGluc3RhbmNlcyBhbmQgZmVhdHVyZXMuDQoNCmBgYHtyfQ0KZGF0YV9leHBsYWluIDwtIHZlY3RvcigibGlzdCIsIG5fY29uZGl0aW9ucykNCm1vZGVsc19jb25kaXRpb24gPC0gdmVjdG9yKCJsaXN0Iiwgbl9jb25kaXRpb25zKQ0KDQpmb3IoaSBpbiAxOm5fY29uZGl0aW9ucykgew0KICANCiAgZGF0YV9jb25kaXRpb24gPC0gZGF0YV90cmFpbltbd2hpY2gobmFtZXMoZGF0YV90cmFpbikgPT0gYXMuY2hhcmFjdGVyKGNvbmRpdGlvbnMkbl9mZWF0dXJlc1tpXSkpXV0NCiAgDQogICMgMSBkYXRhc2V0IG9mIGZlYXR1cmVzIHRvIGJlIGV4cGxhaW5lZCBmb3IgZWFjaCBjb25kaXRpb24uDQogIGRhdGFfZXhwbGFpbltbaV1dIDwtIGRhdGFfY29uZGl0aW9uWzE6Y29uZGl0aW9ucyRuX2luc3RhbmNlc1tpXSwgIW5hbWVzKGRhdGFfY29uZGl0aW9uKSAlaW4lICJ5Il0NCiAgDQogICMgMSBtb2RlbCBmb3IgZWFjaCBjb25kaXRpb24uDQogIG1vZGVsc19jb25kaXRpb25bW2ldXSA8LSBtb2RlbHNbW3doaWNoKG5hbWVzKG1vZGVscykgPT0gYXMuY2hhcmFjdGVyKGNvbmRpdGlvbnMkbl9mZWF0dXJlc1tpXSkpXV0NCn0NCmBgYA0KDQoNCiMjICoqUHJlZGljdCBXcmFwcGVyIEZ1bmN0aW9ucyBpbiBgUmAqKg0KDQoqIFRoZSBgcHJlZGljdCgpYCBmdW5jdGlvbnMgd2lsbCBiZSB3cml0dGVuIGluIGBSYCBhbmQgdGhlbiBjb252ZXJ0ZWQgdG8gYEp1bGlhYCB0byB3b3JrIHdpdGggDQp0aGUgdHJhaW5lZCBSYW5kb20gRm9yZXN0IG1vZGVsLS13aGljaCBpcyBub3QgZXhwbGljaXRseSBjb252ZXJ0ZWQuDQoNCmBgYHtyfQ0KcHJlZGljdF9mdW5fciA8LSBmdW5jdGlvbihvYmplY3QsIG5ld2RhdGEpIHsNCiAgcHJlZGljdChvYmplY3QsIGRhdGEgPSBuZXdkYXRhKSRwcmVkaWN0aW9ucw0KfQ0KDQpwcmVkaWN0X2Z1bl9qdWxpYSA8LSBmdW5jdGlvbihtb2RlbCwgZGF0YSkgew0KICBkYXRhX3ByZWQgPSBkYXRhLmZyYW1lKCJ5X3ByZWQiID0gcHJlZGljdChtb2RlbCwgZGF0YSkkcHJlZGljdGlvbnMpICAjIE11c3QgcmV0dXJuIGEgRGF0YUZyYW1lLg0KICByZXR1cm4oZGF0YV9wcmVkKQ0KfQ0KYGBgDQoNCg0KIyMgKipTaGFwbGV5IFZhbHVlIENhbGN1bGF0aW9ucyoqDQoNCiMjIyAqKmBSYCoqDQoNCmBgYHtyLCBpbmNsdWRlID0gRkFMU0V9DQojIEluaXRpYWxpemUgZnVuY3Rpb24gLSBoaWRkZW4gaW4gcmVuZGVyZWQgbWFya2Rvd24uDQpmYXN0c2hhcDo6ZXhwbGFpbihtb2RlbHNfY29uZGl0aW9uW1sxXV0sDQogICAgICAgICAgICAgICAgICBYID0gZGF0YV9leHBsYWluW1sxXV0sDQogICAgICAgICAgICAgICAgICBwcmVkX3dyYXBwZXIgPSBwcmVkaWN0X2Z1bl9yLA0KICAgICAgICAgICAgICAgICAgbnNpbSA9IG5fbW9udGVfY2FybG8NCiAgICAgICAgICAgICAgICAgICkNCmBgYA0KDQoNCiogRXhwbGFpbiBtb2RlbCBwcmVkaWN0aW9ucyB3aXRoIFNoYXBsZXkgdmFsdWVzIHVzaW5nIGBmYXN0c2hhcGAuDQoNCmBgYHtyLCBldmFsID0gRkFMU0V9DQpkYXRhX3NoYXBfciA8LSB2ZWN0b3IoImxpc3QiLCBuX2NvbmRpdGlvbnMpDQpkYXRhX3NoYXBfciA8LSBsYXBwbHkoZGF0YV9zaGFwX3IsIGZ1bmN0aW9uKHBhc3Mpe3ZlY3RvcigibGlzdCIsIG5fc2ltdWxhdGlvbnMpfSkNCnJ1bnRpbWVfciA8LSB2ZWN0b3IoImxpc3QiLCBuX2NvbmRpdGlvbnMpDQpydW50aW1lX3IgPC0gbGFwcGx5KGRhdGFfc2hhcF9yLCBmdW5jdGlvbihwYXNzKXt2ZWN0b3IoImxpc3QiLCBuX3NpbXVsYXRpb25zKX0pDQoNCmZvciAoaSBpbiAxOm5fY29uZGl0aW9ucykgew0KICBmb3IgKGogaW4gMTpuX3NpbXVsYXRpb25zKSB7DQogICAgDQogICAgICBzZXQuc2VlZChzZWVkKQ0KICAgICAgc3RhcnRfdGltZSA8LSBTeXMudGltZSgpDQogICAgICANCiAgICAgIGRhdGFfc2hhcF9yW1tpXV1bW2pdXSA8LSBmYXN0c2hhcDo6ZXhwbGFpbihtb2RlbHNfY29uZGl0aW9uW1tpXV0sIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFggPSBkYXRhX2V4cGxhaW5bW2ldXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkX3dyYXBwZXIgPSBwcmVkaWN0X2Z1bl9yLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5zaW0gPSBuX21vbnRlX2NhcmxvDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQ0KICAgICAgDQogICAgICBzdG9wX3RpbWUgPC0gU3lzLnRpbWUoKQ0KICAgICAgDQogICAgICBydW50aW1lX3JbW2ldXVtbal1dIDwtIGRhdGEuZnJhbWUoInRpbWUiID0gYXMubnVtZXJpYyhkaWZmdGltZShzdG9wX3RpbWUsIHN0YXJ0X3RpbWUsIHVuaXRzID0gInNlY3MiKSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvbmRpdGlvbiIgPSBpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJzaW11bGF0aW9uIiA9IGopDQogICAgICANCiAgICAgIHByaW50KHJ1bnRpbWVfcltbaV1dW1tqXV0pDQogIH0NCn0NCg0KcnVudGltZV9yIDwtIGRwbHlyOjpiaW5kX3Jvd3ModW5saXN0KHJ1bnRpbWVfciwgcmVjdXJzaXZlID0gRkFMU0UpKQ0KYGBgDQoNCg0KYGBge3IsIGluY2x1ZGUgPSBGQUxTRSwgZXZhbCA9IEZBTFNFfQ0KIyBTYXZlIHJlc3VsdHMgLSBoaWRkZW4gaW4gcmVuZGVyZWQgbWFya2Rvd24uDQpzYXZlKGRhdGFfc2hhcF9yLCBmaWxlID0gImRhdGFfc2hhcF9yLlJkYSIpDQpzYXZlKHJ1bnRpbWVfciwgZmlsZSA9ICJydW50aW1lX3IuUmRhIikNCmBgYA0KDQoNCiMjIyAqKmBKdWxpYWAqKg0KDQoqIExvYWQgYFJgIG9iamVjdHMgaW50byB0aGUgYEp1bGlhYCBlbnZpcm9ubWVudC4NCg0KYGBge2p1bGlhfQ0Kbl9jb25kaXRpb25zID0gUkNhbGwucmV2YWwoIm5fY29uZGl0aW9ucyIpDQpuX2NvbmRpdGlvbnMgPSBjb252ZXJ0KEludGVnZXIsIG5fY29uZGl0aW9ucykNCg0Kbl9zaW11bGF0aW9ucyA9IFJDYWxsLnJldmFsKCJuX3NpbXVsYXRpb25zIikNCm5fc2ltdWxhdGlvbnMgPSBjb252ZXJ0KEludGVnZXIsIG5fc2ltdWxhdGlvbnMpDQoNCm1vZGVsc19jb25kaXRpb24gPSBSQ2FsbC5yZXZhbCgibW9kZWxzX2NvbmRpdGlvbiIpDQoNCnByZWRpY3RfZnVuX2p1bGlhID0gUkNhbGwucmV2YWwoInByZWRpY3RfZnVuX2p1bGlhIikNCnByZWRpY3RfZnVuX2p1bGlhID0gY29udmVydChGdW5jdGlvbiwgcHJlZGljdF9mdW5fanVsaWEpDQoNCmRhdGFfZXhwbGFpbiA9IFJDYWxsLnJldmFsKCJkYXRhX2V4cGxhaW4iKQ0KDQpuX21vbnRlX2NhcmxvID0gUkNhbGwucmV2YWwoIm5fbW9udGVfY2FybG8iKQ0Kbl9tb250ZV9jYXJsbyA9IGNvbnZlcnQoSW50ZWdlciwgbl9tb250ZV9jYXJsbykNCg0Kc2VlZCA9IFJDYWxsLnJldmFsKCJzZWVkIikNCnNlZWQgPSBjb252ZXJ0KEludGVnZXIsIHNlZWQpDQpgYGANCg0KDQpgYGB7anVsaWEsIGluY2x1ZGUgPSBGQUxTRX0NCiMgSW5pdGlhbGl6ZSBmdW5jdGlvbiAtIGhpZGRlbiBpbiByZW5kZXJlZCBtYXJrZG93bi4NCmRhdGFfZXhwbGFpbl90ZW1wID0gY29udmVydChEYXRhRnJhbWUsIGRhdGFfZXhwbGFpblsxXSkNClNoYXBNTC5zaGFwKGV4cGxhaW4gPSBkYXRhX2V4cGxhaW5fdGVtcCwNCiAgICAgICAgICAgIHJlZmVyZW5jZSA9IGRhdGFfZXhwbGFpbl90ZW1wLA0KICAgICAgICAgICAgbW9kZWwgPSBtb2RlbHNfY29uZGl0aW9uWzFdLA0KICAgICAgICAgICAgcHJlZGljdF9mdW5jdGlvbiA9IHByZWRpY3RfZnVuX2p1bGlhLA0KICAgICAgICAgICAgc2FtcGxlX3NpemUgPSBuX21vbnRlX2NhcmxvLA0KICAgICAgICAgICAgc2VlZCA9IHNlZWQNCiAgICAgICAgICAgICkNCmBgYA0KDQoNCiogRXhwbGFpbiBtb2RlbCBwcmVkaWN0aW9ucyB3aXRoIFNoYXBsZXkgdmFsdWVzIHVzaW5nIGBTaGFwTUxgLg0KDQpgYGB7anVsaWEsIGV2YWwgPSBGQUxTRX0NCmRhdGFfc2hhcF9qdWxpYSA9IFtBcnJheXtEYXRhRnJhbWV9KHVuZGVmLCBuX3NpbXVsYXRpb25zKSBmb3IgaSBpbiAxOm5fY29uZGl0aW9uc10NCnJ1bnRpbWVfanVsaWEgPSBbQXJyYXl7QW55fSh1bmRlZiwgbl9zaW11bGF0aW9ucykgZm9yIGkgaW4gMTpuX2NvbmRpdGlvbnNdDQoNCmZvciBpIGluIDE6bl9jb25kaXRpb25zDQogIGZvciBqIGluIDE6bl9zaW11bGF0aW9ucw0KDQogICAgICBkYXRhX2V4cGxhaW5fdGVtcCA9IGNvbnZlcnQoRGF0YUZyYW1lLCBkYXRhX2V4cGxhaW5baV0pDQogICAgICANCiAgICAgIHN0YXJ0X3RpbWUgPSB0aW1lKCkNCiAgICAgIA0KICAgICAgZGF0YV9zaGFwX2p1bGlhW2ldW2pdID0gU2hhcE1MLnNoYXAoZXhwbGFpbiA9IGRhdGFfZXhwbGFpbl90ZW1wLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVmZXJlbmNlID0gZGF0YV9leHBsYWluX3RlbXAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtb2RlbCA9IG1vZGVsc19jb25kaXRpb25baV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkaWN0X2Z1bmN0aW9uID0gcHJlZGljdF9mdW5fanVsaWEsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzYW1wbGVfc2l6ZSA9IG5fbW9udGVfY2FybG8sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWVkID0gc2VlZA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQ0KICAgICAgDQogICAgICBzdG9wX3RpbWUgPSB0aW1lKCkNCiAgICAgIA0KICAgICAgcnVudGltZV9qdWxpYVtpXVtqXSA9IERhdGFGcmFtZSh0aW1lID0gc3RvcF90aW1lIC0gc3RhcnRfdGltZSwgY29uZGl0aW9uID0gaSwgc2ltdWxhdGlvbiA9IGopDQogIGVuZA0KZW5kDQoNCnJ1bnRpbWVfanVsaWEgPSB2Y2F0KHZjYXQocnVudGltZV9qdWxpYS4uLikuLi4pDQpgYGANCg0KDQpgYGB7ciwgaW5jbHVkZSA9IEZBTFNFLCBldmFsID0gRkFMU0V9DQojIENvbnZlcnQgSnVsaWEgb2JqZWN0cyB0byBSIGZvciBzYXZpbmcgLSBoaWRkZW4gaW4gcmVuZGVyZWQgbWFya2Rvd24uDQpkYXRhX3NoYXBfanVsaWFfIDwtIEp1bGlhQ2FsbDo6anVsaWFfZXZhbCgiZGF0YV9zaGFwX2p1bGlhIikNCmRhdGFfc2hhcF9qdWxpYSA8LSB2ZWN0b3IoImxpc3QiLCBsZW5ndGgoZGF0YV9zaGFwX2p1bGlhXykpDQoNCmZvciAoaSBpbiAxOmxlbmd0aChkYXRhX3NoYXBfanVsaWEpKSB7DQogIA0KICBkYXRhX3NoYXBfanVsaWFbW2ldXSA8LSBkYXRhX3NoYXBfanVsaWFfW1tpXV1bWzFdXSAgIyBTYXZlIG9ubHkgMSBvZiB0aGUgc2ltdWxhdGlvbiByZXN1bHRzLg0KICBkYXRhX3NoYXBfanVsaWFbW2ldXSRjb25kaXRpb24gPC0gaQ0KfQ0KDQpkYXRhX3NoYXBfanVsaWEgPC0gZHBseXI6OmJpbmRfcm93cyhkYXRhX3NoYXBfanVsaWEpDQoNCnJ1bnRpbWVfanVsaWEgPC0gSnVsaWFDYWxsOjpqdWxpYV9ldmFsKCJydW50aW1lX2p1bGlhIikNCg0Kc2F2ZShkYXRhX3NoYXBfanVsaWEsIGZpbGUgPSAiZGF0YV9zaGFwX2p1bGlhLlJkYSIpDQpzYXZlKHJ1bnRpbWVfanVsaWEsIGZpbGUgPSAicnVudGltZV9qdWxpYS5SZGEiKQ0KYGBgDQoNCg0KIyMgKipTaW11bGF0aW9uIFJlc3VsdHMqKg0KDQpgYGB7ciwgaW5jbHVkZSA9IEZBTFNFfQ0KIyBMb2FkIHNhdmVkIHJ1bnRpbWUgZGF0YSAtIGhpZGRlbiBpbiByZW5kZXJlZCBtYXJrZG93bi4NCmxvYWQoZmlsZSA9ICJydW50aW1lX3IuUmRhIikNCmxvYWQoZmlsZSA9ICJydW50aW1lX2p1bGlhLlJkYSIpDQpgYGANCg0KDQpgYGB7cn0NCnJ1bnRpbWVfciRsYW5ndWFnZSA8LSAiUiINCnJ1bnRpbWVfanVsaWEkbGFuZ3VhZ2UgPC0gIkp1bGlhIg0KDQpkYXRhX3J1bnRpbWUgPC0gZHBseXI6OmJpbmRfcm93cyhydW50aW1lX3IsIHJ1bnRpbWVfanVsaWEpDQoNCmRhdGFfcnVudGltZSA8LSBkcGx5cjo6bGVmdF9qb2luKGRhdGFfcnVudGltZSwgY29uZGl0aW9ucywgYnkgPSAiY29uZGl0aW9uIikNCmBgYA0KDQoNCiogKipQbG90czoqKg0KICAgICsgKipUb3A6KiogQSBjb21wYXJpc29uIG9mIHJ1bnRpbWVzIGFzIHNhbXBsZSBzaXplcyBpbmNyZWFzZS4NCiAgICArICoqQm90dG9tOioqIEEgY29tcGFyaXNvbiBvZiBydW50aW1lcyBhcyB0aGUgbnVtYmVyIG9mIGZlYXR1cmVzIGluY3JlYXNlcy4NCjxwPg0KKiAqKlJlc3VsdHM6KioNCiAgICArIEZvciBhbGwgb2YgdGhlIHNpbXVsYXRpb25zIGV4YW1pbmVkLCB0aGUgU2hhcGxleSB2YWx1ZSBhcHByb3hpbWF0aW9uIGFsZ29yaXRobSBpbiBgSnVsaWFgJ3MgYFNoYXBNTGAgDQogICAgaXMgY29uc2lzdGVudGx5IGFuZCBub3RpY2VhYmx5IGZhc3RlciB0aGFuIHRoZSBgUmAgaW1wbGVtZW50YXRpb24gaW4gYGZhc3RzaGFwYC4NCg0KYGBge3IsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBmaWcud2lkdGggPSA5LCBmaWcuaGVpZ2h0ID0gNn0NCnBsb3RfdGhlbWUgPC0gdGhlbWUoDQogIHN0cmlwLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyLCBmYWNlID0gImJvbGQiKSwNCiAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplID0gMTQsIGZhY2UgPSAiYm9sZCIpLA0KICBwbG90LnN1YnRpdGxlID0gZWxlbWVudF90ZXh0KHNpemUgPSAxNCksDQogIGF4aXMudGl0bGUgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyLCBmYWNlID0gImJvbGQiKSwNCiAgYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyLCBmYWNlID0gImJvbGQiKSwNCiAgYXhpcy50ZXh0LnkgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyLCBmYWNlID0gImJvbGQiKSwNCiAgbGVnZW5kLnRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyLCBmYWNlID0gImJvbGQiKSwNCiAgbGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSINCikNCg0KcCA8LSBnZ3Bsb3QoZGF0YV9ydW50aW1lLCBhZXMobl9pbnN0YW5jZXMsIHRpbWUsIGNvbG9yID0gbGFuZ3VhZ2UpKQ0KcCA8LSBwICsgZ2VvbV9wb2ludChzaXplID0gNSwgYWxwaGEgPSAuNSkNCnAgPC0gcCArIHN0YXRfc21vb3RoKG1ldGhvZCA9ICJsbSIsIHNlID0gRkFMU0UsIHNob3cubGVnZW5kID0gRkFMU0UsIHNpemUgPSAxLjEsIGFscGhhID0gLjUpDQpwIDwtIHAgKyBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYygiUiIgPSAiIzI3NkRDMiIsICJKdWxpYSIgPSAiIzk1NThCMiIpKQ0KcCA8LSBwICsgZmFjZXRfd3JhcCh+IG5fZmVhdHVyZXMsIHNjYWxlcyA9ICJmcmVlIikNCnAgPC0gcCArIHRoZW1lX2J3KCkgKyBwbG90X3RoZW1lDQpwIDwtIHAgKyB4bGFiKCJOdW1iZXIgb2YgaW5zdGFuY2VzIGluIGRhdGFzZXQiKSArIHlsYWIoIlJ1bnRpbWUgaW4gc2Vjb25kcyIpICsgbGFicyhjb2xvciA9IE5VTEwpICsNCiAgZ2d0aXRsZSgiU2hhcGxleSBBcHByb3hpbWF0aW9uIEFsZ29yaXRobSBSdW50aW1lIC0gSnVsaWEgdnMgUiAoTm9uLVBhcmFsbGVsKSIsDQogICAgICAgICAgc3VidGl0bGUgPSAiRmFjZXRlZCBieSBudW1iZXIgb2YgZmVhdHVyZXMiKQ0KcF8xIDwtIHANCnBfMQ0KDQpwIDwtIGdncGxvdChkYXRhX3J1bnRpbWUsIGFlcyhuX2ZlYXR1cmVzLCB0aW1lLCBjb2xvciA9IGxhbmd1YWdlKSkNCnAgPC0gcCArIGdlb21fcG9pbnQoc2l6ZSA9IDUsIGFscGhhID0gLjUpDQpwIDwtIHAgKyBzdGF0X3Ntb290aChtZXRob2QgPSAibG0iLCBmb3JtdWxhID0geSB+IHBvbHkoeCwgMiksIA0KICAgICAgICAgICAgICAgICAgICAgc2UgPSBGQUxTRSwgc2hvdy5sZWdlbmQgPSBGQUxTRSwgc2l6ZSA9IDEuMSwgYWxwaGEgPSAuNSkNCnAgPC0gcCArIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjKCJSIiA9ICIjMjc2REMyIiwgIkp1bGlhIiA9ICIjOTU1OEIyIikpDQpwIDwtIHAgKyBzY2FsZV94X2NvbnRpbnVvdXMobGltaXRzID0gYygwLCAyMDApKQ0KcCA8LSBwICsgZmFjZXRfd3JhcCh+IG5faW5zdGFuY2VzLCBzY2FsZXMgPSAiZnJlZSIpDQpwIDwtIHAgKyB0aGVtZV9idygpICsgcGxvdF90aGVtZQ0KcCA8LSBwICsgeGxhYigiTnVtYmVyIG9mIGZlYXR1cmVzIGluIGRhdGFzZXQiKSArIHlsYWIoIlJ1bnRpbWUgaW4gc2Vjb25kcyIpICsgbGFicyhjb2xvciA9IE5VTEwpICsNCiAgZ2d0aXRsZSgiU2hhcGxleSBBcHByb3hpbWF0aW9uIEFsZ29yaXRobSBSdW50aW1lIC0gSnVsaWEgdnMgUiAoTm9uLVBhcmFsbGVsKSIsDQogICAgICAgICAgc3VidGl0bGUgPSAiRmFjZXRlZCBieSBudW1iZXIgb2YgaW5zdGFuY2VzIikNCnBfMiA8LSBwDQpwXzINCmBgYA0KDQpgYGB7ciwgaW5jbHVkZSA9IEZBTFNFLCBldmFsID0gRkFMU0V9DQpnZ3NhdmUocF8xLCBmaWxlID0gInNoYXBNTF9mYXN0c2hhcF9zaW1fcGxvdF8xLnBuZyIsIHdpZHRoID0gOSwgaGVpZ2h0ID0gNikNCmdnc2F2ZShwXzIsIGZpbGUgPSAic2hhcE1MX2Zhc3RzaGFwX3NpbV9wbG90XzIucG5nIiwgd2lkdGggPSA5LCBoZWlnaHQgPSA2KQ0KZ2dzYXZlKHBfMSwgZmlsZSA9ICJzaGFwTUxfZmFzdHNoYXBfc2ltX3Bsb3RfMV9zdW1tYXJ5LnBuZyIsIHdpZHRoID0gOSwgaGVpZ2h0ID0gNCkNCmdnc2F2ZShwXzIsIGZpbGUgPSAic2hhcE1MX2Zhc3RzaGFwX3NpbV9wbG90XzJfc3VtbWFyeS5wbmciLCB3aWR0aCA9IDksIGhlaWdodCA9IDQpDQpgYGANCg0K