Introduction
Bộ số liệu German Credit đã được sử dụng trong nhiều reseach paper. Một số kết quả thực nghiệm với bộ số liệu này được nhắc lại trong nghiên cứu có tên Investigation and improvement of multi-layer perceptron neural networks for credit scoring như sau:

Với các thuật toán Machine Learning thì khối lượng tính toán là rất lớn và các thuật toán có thể cần hàng giờ đến hàng chục giờ để chạy. Con số này có thể lớn hơn rất nhiều nếu bộ số liệu có kích thước lớn và nhiều biến số đầu vào. Vì lí do này, lựa chọn các biến đầu vào phù hợp hoặc sử dụng các kĩ thuật cho Feature Engineering có thể hướng đến đồng thời cả hai mục đích: (1) giảm số lượng biến đầu vào cần thiết để xây dựng mô hình và dẫn đến giảm thời gian tính toán, và (2) đồng thời cải thiện chất lượng dự báo của mô hình hoặc đảm bảo rằng chất lượng dự báo của mô hình giảm không đáng kể trong khi số lượng biến sử dụng lại giảm đi rất nhiều.
Với bộ số liệu German Credit ở trên kết quả chỉ ra rằng:
Mức độ chính xác trung bình cho 10 lần thử nghiệm bằng Cross - Validation là 79% (một kết quả thuộc loại khá nếu chúng ta so sánh với các kết quả ở các nghiên cứu khác) bằng cách sử dụng Random Forest (RF) không tinh chỉnh gì. Kết quả này có thể cải thiện hơn nữa nếu chúng ta tinh chỉnh các tham số cho RF.
Autoencoder có thể được sử dụng hiệu quả như là một phương pháp cho Feature Engineering.
Default Random Forest
R codes dưới đây thực hiện huấn luyện RF không tinh chỉnh đồng thời khảo sát kết quả thu được trên 10 mẫu bằng kĩ thuật Cross-Validation:
## Connection successful!
##
## R is connected to the H2O cluster:
## H2O cluster uptime: 27 minutes 58 seconds
## H2O cluster timezone: Asia/Bangkok
## H2O data parsing timezone: UTC
## H2O cluster version: 3.20.0.2
## H2O cluster version age: 5 months and 26 days !!!
## H2O cluster name: H2O_started_from_R_Zbook_waz726
## H2O cluster total nodes: 1
## H2O cluster total memory: 13.84 GB
## H2O cluster total cores: 8
## H2O cluster allowed cores: 8
## H2O cluster healthy: TRUE
## H2O Connection ip: localhost
## H2O Connection port: 54321
## H2O Connection proxy: NA
## H2O Internal Security: FALSE
## H2O API Extensions: Algos, AutoML, Core V3, Core V4
## R Version: R version 3.5.1 (2018-07-02)
h2o.no_progress()
# Convert to H2o Frame:
train <- as.h2o(df_train)
test <- as.h2o(df_test)
# Identify inputput and output:
y <- "Class"
x <- setdiff(names(train), y)
# Train Default Random Forest:
pure_nn <- h2o.randomForest(x = x, y = y,
training_frame = train,
nfolds = 10,
stopping_rounds = 5,
stopping_metric = "AUC",
seed = 29)
# Function collects results from cross-validation:
results_df <- function(h2o_model) {
h2o_model@model$cross_validation_metrics_summary %>%
as.data.frame() %>%
select(-mean, -sd) %>%
t() %>%
as.data.frame() %>%
mutate_all(as.character) %>%
mutate_all(as.numeric) %>%
select(Accuracy = accuracy,
AUC = auc,
Precision = precision,
Specificity = specificity,
Recall = recall,
Logloss = logloss) %>%
return()
}
# Function presents results by graph:
vis_results <- function(df_results) {
df_results %>%
gather(Metrics, Values) %>%
ggplot(aes(Metrics, Values, fill = Metrics, color = Metrics)) +
geom_boxplot(alpha = 0.3, show.legend = FALSE) +
facet_wrap(~ Metrics, scales = "free") +
scale_y_continuous(labels = scales::percent) +
theme_minimal() +
labs(x = NULL, y = NULL,
title = "Model Performance Based on Cross Validation")
}
pure_nn %>% results_df() %>% summary()
## Accuracy AUC Precision Specificity
## Min. :0.6857 Min. :0.7067 Min. :0.6667 Min. :0.1538
## 1st Qu.:0.7605 1st Qu.:0.7664 1st Qu.:0.7794 1st Qu.:0.3750
## Median :0.7887 Median :0.7805 Median :0.8038 Median :0.4603
## Mean :0.7896 Mean :0.7963 Mean :0.8008 Mean :0.4546
## 3rd Qu.:0.8194 3rd Qu.:0.8404 3rd Qu.:0.8468 3rd Qu.:0.5395
## Max. :0.8659 Max. :0.8645 Max. :0.8750 Max. :0.6857
## Recall Logloss
## Min. :0.7843 Min. :0.4048
## 1st Qu.:0.9216 1st Qu.:0.4304
## Median :0.9382 Median :0.5425
## Mean :0.9324 Mean :0.6132
## 3rd Qu.:0.9673 3rd Qu.:0.7855
## Max. :1.0000 Max. :0.9896

Nếu chọn Accuracy làm tiêu chí đánh giá thì mô hình RF không tinh chỉnh như trên có trung bình Accuracy là xấp xỉ 79%.
Autoencoder as a Feature Engineering Technique
Sử dụng Autoencoder cho mục đích Feature Engineering với bộ số liệu German Credit cho kết quả như sau:
# Buil a autoencoder:
autoencoder <- h2o.deeplearning(x = x,
training_frame = train,
autoencoder = TRUE,
seed = 29,
hidden = c(10, 20, 61),
epochs = 30,
activation = "Tanh")
#============================================================
# Use Autoencoder as Feature Engineering Method (Version 1)
#============================================================
train_autoen <- h2o.predict(autoencoder, train) %>%
as.data.frame() %>%
mutate(Class = df_train$Class) %>%
as.h2o()
test_autoen <- h2o.predict(autoencoder, test) %>%
as.data.frame() %>%
mutate(Class = df_test$Class) %>%
as.h2o()
nn_autoen_layers1 <- h2o.randomForest(x = setdiff(colnames(train_autoen), "Class"),
y = y,
training_frame = train_autoen,
nfolds = 10,
stopping_rounds = 5,
stopping_metric = "AUC",
seed = 29)
#============================================================
# Use Autoencoder as Feature Engineering Method (Version 2)
#============================================================
train_features_l2 <- h2o.deepfeatures(autoencoder, train, layer = 2) %>%
as.data.frame() %>%
mutate(Class = df_train$Class) %>%
as.h2o()
test_features_l2 <- h2o.deepfeatures(autoencoder, test, layer = 2) %>%
as.data.frame() %>%
mutate(Class = df_test$Class) %>%
as.h2o()
nn_autoen_layers2 <- h2o.randomForest(x = setdiff(colnames(train_features_l2), "Class"),
y = y,
training_frame = train_features_l2,
nfolds = 10,
stopping_rounds = 5,
stopping_metric = "AUC",
seed = 29)
#============================================================
# Use Autoencoder as Feature Engineering Method (Version 3)
#============================================================
train_features_l3 <- h2o.deepfeatures(autoencoder, train, layer = 3) %>%
as.data.frame() %>%
mutate(Class = df_train$Class) %>%
as.h2o()
test_features_l3 <- h2o.deepfeatures(autoencoder, test, layer = 3) %>%
as.data.frame() %>%
mutate(Class = df_test$Class) %>%
as.h2o()
nn_autoen_layers3 <- h2o.randomForest(x = setdiff(colnames(train_features_l3), "Class"),
y = y,
training_frame = train_features_l3,
nfolds = 10,
stopping_rounds = 5,
stopping_metric = "AUC",
seed = 29)
#==========================
# Compare between models
#==========================
do.call("bind_rows",
lapply(list(pure_nn,
nn_autoen_layers1,
nn_autoen_layers2,
nn_autoen_layers3), results_df)) -> df_com
df_com %<>% mutate(Model = rep(c("Original", "Layer1", "20Var", "61Var"), each = 10, time = 1))
theme_set(theme_minimal())
df_com %>%
gather(a, b, -Model) %>%
ggplot(aes(Model, b, fill = Model, color = Model)) +
geom_boxplot(alpha = 0.3) +
scale_y_continuous(labels = scales::percent) +
facet_wrap(~ a, scales = "free") +
labs(x = NULL, y = NULL, title = "Model Performance")

Original |
0.790 |
0.796 |
0.613 |
0.801 |
0.932 |
0.455 |
Layer1 |
0.756 |
0.737 |
0.879 |
0.758 |
0.964 |
0.278 |
61Var |
0.743 |
0.732 |
0.621 |
0.751 |
0.950 |
0.236 |
20Var |
0.726 |
0.696 |
0.837 |
0.724 |
0.987 |
0.124 |
Kết quả này chỉ ra rằng: Autoencoder có thể được sử dụng hiệu quả như là một kĩ thuật cho Feature Engineering.
Optimal Threshold
RF sử dụng toàn bộ 61 biến số nguyên bản là mô hình cho Accuracy cao nhất. Chúng ta có thể khảo sát sự biến đổi các tiêu chí đánh giá chất lượng mô hình phân loại khi ngưỡng được lựa chọn dán nhãn (Bad hay Good) thay đổi như sau:
eval_fun <- function(thre) {
lapply(1:10, function(x) {
set.seed(x)
id <- createDataPartition(y = df_test$Class, p = 0.5, list = FALSE)
test_df <- df_test[id, ]
du_bao_prob <- h2o.predict(pure_nn, test_df %>% as.h2o()) %>%
as.data.frame() %>%
pull(Bad)
du_bao <- case_when(du_bao_prob >= thre ~ "Bad",
du_bao_prob < thre ~ "Good") %>% as.factor()
cm <- confusionMatrix(du_bao, test_df$Class, positive = "Bad")
bg_gg <- cm$table %>%
as.vector() %>%
matrix(ncol = 4) %>%
as.data.frame() %>%
rename(TP = V1, FN = V2, FP = V3, TN = V4)
kq <- c(cm$overall, cm$byClass)
ten <- kq %>% as.data.frame() %>% row.names()
kq %>%
as.vector() %>%
matrix(ncol = 18) %>%
as.data.frame() -> all_df
names(all_df) <- ten
all_df <- bind_cols(all_df, bg_gg)
return(all_df)
})
}
# Đánh giá sự biến đổi theo một loạt ngưỡng:
system.time(so_sanh_list <- lapply(seq(0.05, 0.8, by = 0.05), eval_fun))
## user system elapsed
## 94.45 3.05 138.31
so_sanh_df <- do.call("bind_rows", so_sanh_list)
so_sanh_df %<>%
mutate(Threshold = lapply(seq(0.05, 0.8, by = 0.05), function(x) {rep(x, 10)}) %>% unlist())
theme_set(theme_minimal())
so_sanh_df %>%
group_by(Threshold) %>%
summarise_each(funs(median), Accuracy, Kappa, Sensitivity, Specificity) %>%
gather(Metric, b, -Threshold) %>%
ggplot(aes(Threshold, b, color = Metric)) +
geom_line() +
geom_point(size = 3) +
scale_y_continuous(labels = scales::percent) +
theme(panel.grid.minor = element_blank()) +
scale_x_continuous(breaks = seq(0.05, 0.8, by = 0.05)) +
labs(y = "Accuracy Rate",
title = "Variation of Classifier's Metrics by Threshold")

Chúng ta có thể thấy rõ sự đánh đổi ở đây: khẳ năng phân loại cho nhãn Bad càng cao (Sensitivity) càng cao thì khả năng phân loại cho nhãn Good (Specificity) càng thấp. Điều này dẫn đến Accuracy có dạng hình chữ U ngược với mức cực đại đạt được khi ngưỡng được chọn là 0.7.
Chúng ta có thể hình ảnh hóa khả năng phân loại của mô hình cho hai loại hồ sơ như sau:
my_cm_com_dl <- function(thre) {
du_bao_prob <- h2o.predict(pure_nn, test) %>% as.data.frame() %>% pull(Bad)
du_bao <- case_when(du_bao_prob >= thre ~ "Bad",
du_bao_prob < thre ~ "Good") %>% as.factor()
cm <- confusionMatrix(du_bao, df_test$Class, positive = "Bad")
return(cm)
}
my_threshold <- c(0.10, 0.25, 0.5, 0.7)
results_list_dl <- lapply(my_threshold, my_cm_com_dl)
vis_detection_rate_dl <- function(x) {
results_list_dl[[x]]$table %>% as.data.frame() -> m
rate <- round(100*m$Freq[1] / sum(m$Freq[c(1, 2)]), 2)
acc <- round(100*sum(m$Freq[c(1, 4)]) / sum(m$Freq), 2)
acc <- paste0(acc, "%")
m %>%
ggplot(aes(Reference, Freq, fill = Prediction)) +
geom_col(position = "fill") +
scale_fill_manual(values = c("#e41a1c", "#377eb8"), name = "") +
theme(panel.grid.minor.y = element_blank()) +
theme(panel.grid.minor.x = element_blank()) +
scale_y_continuous(labels = scales::percent) +
labs(x = NULL, y = NULL,
title = paste0("Detecting Bad Cases when Threshold = ", my_threshold[x]),
subtitle = paste0("Detecting Rate for Bad Cases: ", rate, "%", ", ", "Accuracy: ", acc))
}
gridExtra::grid.arrange(vis_detection_rate_dl(1),
vis_detection_rate_dl(2),
vis_detection_rate_dl(3),
vis_detection_rate_dl(4))

LS0tDQp0aXRsZTogIlVzZSBBdXRvZW5jb2RlciBhcyBhIEZlYXR1cmUgRW5naW5lZXJpbmcgVGVjaG5pcXVlIiANCnN1YnRpdGxlOiAiRm9yIEtpbGxpbmcgVGltZSBhdCB0aGUgSG9zcGl0YWwiDQphdXRob3I6ICJOZ3V5ZW4gQ2hpIER1bmciDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICAjIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogcHlnbWVudHMNCiAgICAjIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJmbGF0bHkiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UpDQpgYGANCg0KIyBJbnRyb2R1Y3Rpb24NCg0KQuG7mSBz4buRIGxp4buHdSAqKkdlcm1hbiBDcmVkaXQqKiDEkcOjIMSRxrDhu6NjIHPhu60gZOG7pW5nIHRyb25nIG5oaeG7gXUgcmVzZWFjaCBwYXBlci4gTeG7mXQgc+G7kSBr4bq/dCBxdeG6oyB0aOG7sWMgbmdoaeG7h20gduG7m2kgYuG7mSBz4buRIGxp4buHdSBuw6B5IMSRxrDhu6NjIG5o4bqvYyBs4bqhaSB0cm9uZyBuZ2hpw6puIGPhu6l1IGPDsyB0w6puIFtJbnZlc3RpZ2F0aW9uIGFuZCBpbXByb3ZlbWVudCBvZiBtdWx0aS1sYXllciBwZXJjZXB0cm9uIG5ldXJhbCBuZXR3b3JrcyBmb3IgY3JlZGl0IHNjb3JpbmddKGh0dHBzOi8vd3d3LnNjaWVuY2VkaXJlY3QuY29tL3NjaWVuY2UvYXJ0aWNsZS9waWkvUzA5NTc0MTc0MTQwMDc3MjYpIG5oxrAgc2F1OiANCg0KIVtdKEM6L1VzZXJzL1pib29rL0RvY3VtZW50cy9yZXN1bHRfZ2VybWFuLnBuZykNCg0KVuG7m2kgY8OhYyB0aHXhuq10IHRvw6FuIE1hY2hpbmUgTGVhcm5pbmcgdGjDrCBraOG7kWkgbMaw4bujbmcgdMOtbmggdG/DoW4gbMOgIHLhuqV0IGzhu5tuIHbDoCBjw6FjIHRodeG6rXQgdG/DoW4gY8OzIHRo4buDIGPhuqduIGjDoG5nIGdp4budIMSR4bq/biBow6BuZyBjaOG7pWMgZ2nhu50gxJHhu4MgY2jhuqF5LiBDb24gc+G7kSBuw6B5IGPDsyB0aOG7gyBs4bubbiBoxqFuIHLhuqV0IG5oaeG7gXUgbuG6v3UgYuG7mSBz4buRIGxp4buHdSBjw7Mga8OtY2ggdGjGsOG7m2MgbOG7m24gdsOgIG5oaeG7gXUgYmnhur9uIHPhu5EgxJHhuqd1IHbDoG8uIFbDrCBsw60gZG8gbsOgeSwgbOG7sWEgY2jhu41uIGPDoWMgYmnhur9uIMSR4bqndSB2w6BvIHBow7kgaOG7o3AgaG/hurdjIHPhu60gZOG7pW5nIGPDoWMga8SpIHRodeG6rXQgY2hvIEZlYXR1cmUgRW5naW5lZXJpbmcgY8OzIHRo4buDIGjGsOG7m25nIMSR4bq/biDEkeG7k25nIHRo4budaSBj4bqjIGhhaSBt4bulYyDEkcOtY2g6ICgxKSBnaeG6o20gc+G7kSBsxrDhu6NuZyBiaeG6v24gxJHhuqd1IHbDoG8gY+G6p24gdGhp4bq/dCDEkeG7gyB4w6J5IGThu7FuZyBtw7QgaMOsbmggdsOgIGThuqtuIMSR4bq/biBnaeG6o20gdGjhu51pIGdpYW4gdMOtbmggdG/DoW4sIHbDoCAoMikgxJHhu5NuZyB0aOG7nWkgY+G6o2kgdGhp4buHbiBjaOG6pXQgbMaw4bujbmcgZOG7sSBiw6FvIGPhu6dhIG3DtCBow6xuaCBob+G6t2MgxJHhuqNtIGLhuqNvIHLhurFuZyBjaOG6pXQgbMaw4bujbmcgZOG7sSBiw6FvIGPhu6dhIG3DtCBow6xuaCBnaeG6o20ga2jDtG5nIMSRw6FuZyBr4buDIHRyb25nIGtoaSBz4buRIGzGsOG7o25nIGJp4bq/biBz4butIGThu6VuZyBs4bqhaSBnaeG6o20gxJFpIHLhuqV0IG5oaeG7gXUuIA0KDQpW4bubaSBi4buZIHPhu5EgbGnhu4d1IEdlcm1hbiBDcmVkaXQg4bufIHRyw6puIGvhur90IHF14bqjIGNo4buJIHJhIHLhurFuZzogDQoNCjEuIE3hu6ljIMSR4buZIGNow61uaCB4w6FjIHRydW5nIGLDrG5oIGNobyAxMCBs4bqnbiB0aOG7rSBuZ2hp4buHbSBi4bqxbmcgQ3Jvc3MgLSBWYWxpZGF0aW9uIGzDoCA3OSUgKG3hu5l0IGvhur90IHF14bqjIHRodeG7mWMgbG/huqFpIGtow6EgbuG6v3UgY2jDum5nIHRhIHNvIHPDoW5oIHbhu5tpIGPDoWMga+G6v3QgcXXhuqMg4bufIGPDoWMgbmdoacOqbiBj4bupdSBraMOhYykgYuG6sW5nIGPDoWNoIHPhu60gZOG7pW5nIFJhbmRvbSBGb3Jlc3QgKFJGKSBraMO0bmcgdGluaCBjaOG7iW5oIGfDrC4gS+G6v3QgcXXhuqMgbsOgeSBjw7MgdGjhu4MgY+G6o2kgdGhp4buHbiBoxqFuIG7hu69hIG7hur91IGNow7puZyB0YSB0aW5oIGNo4buJbmggY8OhYyB0aGFtIHPhu5EgY2hvIFJGLiANCg0KDQoyLiBBdXRvZW5jb2RlciBjw7MgdGjhu4MgxJHGsOG7o2Mgc+G7rSBk4bulbmcgaGnhu4d1IHF14bqjIG5oxrAgbMOgIG3hu5l0IHBoxrDGoW5nIHBow6FwIGNobyBGZWF0dXJlIEVuZ2luZWVyaW5nLiANCg0KIyBEZWZhdWx0IFJhbmRvbSBGb3Jlc3QNCg0KUiBjb2RlcyBkxrDhu5tpIMSRw6J5IHRo4buxYyBoaeG7h24gaHXhuqVuIGx1eeG7h24gUkYga2jDtG5nIHRpbmggY2jhu4luaCDEkeG7k25nIHRo4budaSBraOG6o28gc8OhdCBr4bq/dCBxdeG6oyB0aHUgxJHGsOG7o2MgdHLDqm4gMTAgbeG6q3UgYuG6sW5nIGvEqSB0aHXhuq10IENyb3NzLVZhbGlkYXRpb246IA0KDQoNCmBgYHtyLCBmaWcuZnVsbHdpZHRoID0gVFJVRSwgZmlnLmhlaWdodD04LCBmaWcud2lkdGg9MTJ9DQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBTdGFnZSAxOiBEYXRhIFByZS1wcm9jZXNzaW5nDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgQ2xlYXIgd29ya3NwYWNlOiANCnJtKGxpc3QgPSBscygpKQ0KDQojIExvYWQgcGFja2FnZXMgYW5kIGRhdGE6IA0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KG1hZ3JpdHRyKQ0KbGlicmFyeShjYXJldCkNCmRhdGEoIkdlcm1hbkNyZWRpdCIpDQoNCiMgU3BsaXQgZGF0YTogDQpzZXQuc2VlZCgxKQ0KaWQgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbih5ID0gR2VybWFuQ3JlZGl0JENsYXNzLCBwID0gMC44LCBsaXN0ID0gRkFMU0UpDQpkZl90cmFpbiA8LSBHZXJtYW5DcmVkaXRbaWQsIF0gIyBGb3IgdHJhaW5pbmcNCmRmX3Rlc3QgPC0gR2VybWFuQ3JlZGl0Wy1pZCwgXSAjIEZvciB0ZXN0aW5nDQoNCg0KIyBBY3RpdmF0ZSBoMm8gcGFja2FnZSBmb3IgZGVlcCBsZWFybmluZzogDQpsaWJyYXJ5KGgybykNCmgyby5pbml0KG50aHJlYWRzID0gLTEsIG1heF9tZW1fc2l6ZSA9ICIxNmciKQ0KaDJvLm5vX3Byb2dyZXNzKCkNCg0KIyBDb252ZXJ0IHRvIEgybyBGcmFtZToNCnRyYWluIDwtIGFzLmgybyhkZl90cmFpbikNCnRlc3QgPC0gYXMuaDJvKGRmX3Rlc3QpDQoNCiMgSWRlbnRpZnkgaW5wdXRwdXQgYW5kIG91dHB1dDogDQp5IDwtICJDbGFzcyINCnggPC0gc2V0ZGlmZihuYW1lcyh0cmFpbiksIHkpDQoNCiMgVHJhaW4gRGVmYXVsdCBSYW5kb20gRm9yZXN0OiANCnB1cmVfbm4gPC0gaDJvLnJhbmRvbUZvcmVzdCh4ID0geCwgeSA9IHksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluaW5nX2ZyYW1lID0gdHJhaW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgbmZvbGRzID0gMTAsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0b3BwaW5nX3JvdW5kcyA9IDUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0b3BwaW5nX21ldHJpYyA9ICJBVUMiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzZWVkID0gMjkpDQoNCg0KIyBGdW5jdGlvbiBjb2xsZWN0cyByZXN1bHRzIGZyb20gY3Jvc3MtdmFsaWRhdGlvbjogDQoNCnJlc3VsdHNfZGYgPC0gZnVuY3Rpb24oaDJvX21vZGVsKSB7DQogIGgyb19tb2RlbEBtb2RlbCRjcm9zc192YWxpZGF0aW9uX21ldHJpY3Nfc3VtbWFyeSAlPiUgDQogICAgYXMuZGF0YS5mcmFtZSgpICU+JSANCiAgICBzZWxlY3QoLW1lYW4sIC1zZCkgJT4lIA0KICAgIHQoKSAlPiUgDQogICAgYXMuZGF0YS5mcmFtZSgpICU+JSANCiAgICBtdXRhdGVfYWxsKGFzLmNoYXJhY3RlcikgJT4lIA0KICAgIG11dGF0ZV9hbGwoYXMubnVtZXJpYykgJT4lIA0KICAgIHNlbGVjdChBY2N1cmFjeSA9IGFjY3VyYWN5LCANCiAgICAgICAgICAgQVVDID0gYXVjLCANCiAgICAgICAgICAgUHJlY2lzaW9uID0gcHJlY2lzaW9uLCANCiAgICAgICAgICAgU3BlY2lmaWNpdHkgPSBzcGVjaWZpY2l0eSwgDQogICAgICAgICAgIFJlY2FsbCA9IHJlY2FsbCwgDQogICAgICAgICAgIExvZ2xvc3MgPSBsb2dsb3NzKSAlPiUgDQogICAgcmV0dXJuKCkNCiAgfQ0KDQoNCiMgRnVuY3Rpb24gcHJlc2VudHMgcmVzdWx0cyBieSBncmFwaDogDQoNCnZpc19yZXN1bHRzIDwtIGZ1bmN0aW9uKGRmX3Jlc3VsdHMpIHsNCiAgZGZfcmVzdWx0cyAlPiUgDQogICAgZ2F0aGVyKE1ldHJpY3MsIFZhbHVlcykgJT4lIA0KICAgIGdncGxvdChhZXMoTWV0cmljcywgVmFsdWVzLCBmaWxsID0gTWV0cmljcywgY29sb3IgPSBNZXRyaWNzKSkgKw0KICAgIGdlb21fYm94cGxvdChhbHBoYSA9IDAuMywgc2hvdy5sZWdlbmQgPSBGQUxTRSkgKyANCiAgICBmYWNldF93cmFwKH4gTWV0cmljcywgc2NhbGVzID0gImZyZWUiKSArIA0KICAgIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsgDQogICAgdGhlbWVfbWluaW1hbCgpICsgDQogICAgbGFicyh4ID0gTlVMTCwgeSA9IE5VTEwsIA0KICAgICAgICAgdGl0bGUgPSAiTW9kZWwgUGVyZm9ybWFuY2UgQmFzZWQgb24gQ3Jvc3MgVmFsaWRhdGlvbiIpDQp9DQoNCg0KcHVyZV9ubiAlPiUgcmVzdWx0c19kZigpICU+JSBzdW1tYXJ5KCkNCnB1cmVfbm4gJT4lIHJlc3VsdHNfZGYoKSAlPiUgdmlzX3Jlc3VsdHMoKQ0KDQpgYGANCg0KTuG6v3UgY2jhu41uIEFjY3VyYWN5IGzDoG0gdGnDqnUgY2jDrSDEkcOhbmggZ2nDoSB0aMOsIG3DtCBow6xuaCBSRiBraMO0bmcgdGluaCBjaOG7iW5oIG5oxrAgdHLDqm4gY8OzIHRydW5nIGLDrG5oIEFjY3VyYWN5IGzDoCB44bqlcCB44buJIDc5JS4gDQoNCg0KIyBBdXRvZW5jb2RlciBhcyBhIEZlYXR1cmUgRW5naW5lZXJpbmcgVGVjaG5pcXVlDQoNClPhu60gZOG7pW5nIEF1dG9lbmNvZGVyIGNobyBt4bulYyDEkcOtY2ggRmVhdHVyZSBFbmdpbmVlcmluZyB24bubaSBi4buZIHPhu5EgbGnhu4d1IEdlcm1hbiBDcmVkaXQgY2hvIGvhur90IHF14bqjIG5oxrAgc2F1OiANCg0KYGBge3IsIGZpZy5mdWxsd2lkdGggPSBUUlVFLCBmaWcuaGVpZ2h0PTgsIGZpZy53aWR0aD0xMn0NCg0KIyBCdWlsIGEgYXV0b2VuY29kZXI6IA0KDQphdXRvZW5jb2RlciA8LSBoMm8uZGVlcGxlYXJuaW5nKHggPSB4LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbmluZ19mcmFtZSA9IHRyYWluLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYXV0b2VuY29kZXIgPSBUUlVFLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZCA9IDI5LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaGlkZGVuID0gYygxMCwgMjAsIDYxKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVwb2NocyA9IDMwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWN0aXZhdGlvbiA9ICJUYW5oIikNCg0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KIyAgVXNlIEF1dG9lbmNvZGVyIGFzIEZlYXR1cmUgRW5naW5lZXJpbmcgTWV0aG9kIChWZXJzaW9uIDEpDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCg0KdHJhaW5fYXV0b2VuIDwtIGgyby5wcmVkaWN0KGF1dG9lbmNvZGVyLCB0cmFpbikgJT4lIA0KICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICBtdXRhdGUoQ2xhc3MgPSBkZl90cmFpbiRDbGFzcykgJT4lIA0KICBhcy5oMm8oKQ0KDQp0ZXN0X2F1dG9lbiA8LSBoMm8ucHJlZGljdChhdXRvZW5jb2RlciwgdGVzdCkgJT4lIA0KICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICBtdXRhdGUoQ2xhc3MgPSBkZl90ZXN0JENsYXNzKSAlPiUgDQogIGFzLmgybygpDQoNCg0Kbm5fYXV0b2VuX2xheWVyczEgPC0gaDJvLnJhbmRvbUZvcmVzdCh4ID0gc2V0ZGlmZihjb2xuYW1lcyh0cmFpbl9hdXRvZW4pLCAiQ2xhc3MiKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHkgPSB5LCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJhaW5pbmdfZnJhbWUgPSB0cmFpbl9hdXRvZW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5mb2xkcyA9IDEwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RvcHBpbmdfcm91bmRzID0gNSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0b3BwaW5nX21ldHJpYyA9ICJBVUMiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZCA9IDI5KQ0KDQoNCg0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KIyAgVXNlIEF1dG9lbmNvZGVyIGFzIEZlYXR1cmUgRW5naW5lZXJpbmcgTWV0aG9kIChWZXJzaW9uIDIpDQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCnRyYWluX2ZlYXR1cmVzX2wyIDwtIGgyby5kZWVwZmVhdHVyZXMoYXV0b2VuY29kZXIsIHRyYWluLCBsYXllciA9IDIpICU+JQ0KICBhcy5kYXRhLmZyYW1lKCkgJT4lDQogIG11dGF0ZShDbGFzcyA9IGRmX3RyYWluJENsYXNzKSAlPiUgDQogIGFzLmgybygpDQoNCg0KdGVzdF9mZWF0dXJlc19sMiA8LSBoMm8uZGVlcGZlYXR1cmVzKGF1dG9lbmNvZGVyLCB0ZXN0LCBsYXllciA9IDIpICU+JQ0KICBhcy5kYXRhLmZyYW1lKCkgJT4lDQogIG11dGF0ZShDbGFzcyA9IGRmX3Rlc3QkQ2xhc3MpICU+JSANCiAgYXMuaDJvKCkgICANCg0KDQpubl9hdXRvZW5fbGF5ZXJzMiA8LSBoMm8ucmFuZG9tRm9yZXN0KHggPSBzZXRkaWZmKGNvbG5hbWVzKHRyYWluX2ZlYXR1cmVzX2wyKSwgIkNsYXNzIiksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB5ID0geSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRyYWluaW5nX2ZyYW1lID0gdHJhaW5fZmVhdHVyZXNfbDIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5mb2xkcyA9IDEwLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RvcHBpbmdfcm91bmRzID0gNSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0b3BwaW5nX21ldHJpYyA9ICJBVUMiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZCA9IDI5KQ0KDQoNCiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCiMgIFVzZSBBdXRvZW5jb2RlciBhcyBGZWF0dXJlIEVuZ2luZWVyaW5nIE1ldGhvZCAoVmVyc2lvbiAzKQ0KIz09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KDQp0cmFpbl9mZWF0dXJlc19sMyA8LSBoMm8uZGVlcGZlYXR1cmVzKGF1dG9lbmNvZGVyLCB0cmFpbiwgbGF5ZXIgPSAzKSAlPiUNCiAgYXMuZGF0YS5mcmFtZSgpICU+JQ0KICBtdXRhdGUoQ2xhc3MgPSBkZl90cmFpbiRDbGFzcykgJT4lIA0KICBhcy5oMm8oKQ0KDQoNCnRlc3RfZmVhdHVyZXNfbDMgPC0gaDJvLmRlZXBmZWF0dXJlcyhhdXRvZW5jb2RlciwgdGVzdCwgbGF5ZXIgPSAzKSAlPiUNCiAgYXMuZGF0YS5mcmFtZSgpICU+JQ0KICBtdXRhdGUoQ2xhc3MgPSBkZl90ZXN0JENsYXNzKSAlPiUgDQogIGFzLmgybygpDQoNCg0Kbm5fYXV0b2VuX2xheWVyczMgPC0gaDJvLnJhbmRvbUZvcmVzdCh4ID0gc2V0ZGlmZihjb2xuYW1lcyh0cmFpbl9mZWF0dXJlc19sMyksICJDbGFzcyIpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeSA9IHksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0cmFpbmluZ19mcmFtZSA9IHRyYWluX2ZlYXR1cmVzX2wzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZm9sZHMgPSAxMCwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0b3BwaW5nX3JvdW5kcyA9IDUsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdG9wcGluZ19tZXRyaWMgPSAiQVVDIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWQgPSAyOSkNCg0KIz09PT09PT09PT09PT09PT09PT09PT09PT09DQojICBDb21wYXJlIGJldHdlZW4gbW9kZWxzDQojPT09PT09PT09PT09PT09PT09PT09PT09PT0NCg0KZG8uY2FsbCgiYmluZF9yb3dzIiwgDQogICAgICAgIGxhcHBseShsaXN0KHB1cmVfbm4sIA0KICAgICAgICAgICAgICAgICAgICBubl9hdXRvZW5fbGF5ZXJzMSwgDQogICAgICAgICAgICAgICAgICAgIG5uX2F1dG9lbl9sYXllcnMyLCANCiAgICAgICAgICAgICAgICAgICAgbm5fYXV0b2VuX2xheWVyczMpLCByZXN1bHRzX2RmKSkgLT4gZGZfY29tDQoNCg0KZGZfY29tICU8PiUgbXV0YXRlKE1vZGVsID0gcmVwKGMoIk9yaWdpbmFsIiwgIkxheWVyMSIsICIyMFZhciIsICI2MVZhciIpLCBlYWNoID0gMTAsIHRpbWUgPSAxKSkNCg0KdGhlbWVfc2V0KHRoZW1lX21pbmltYWwoKSkNCmRmX2NvbSAlPiUgDQogIGdhdGhlcihhLCBiLCAtTW9kZWwpICU+JSANCiAgZ2dwbG90KGFlcyhNb2RlbCwgYiwgZmlsbCA9IE1vZGVsLCBjb2xvciA9IE1vZGVsKSkgKyANCiAgZ2VvbV9ib3hwbG90KGFscGhhID0gMC4zKSArIA0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSArIA0KICBmYWNldF93cmFwKH4gYSwgc2NhbGVzID0gImZyZWUiKSArIA0KICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgdGl0bGUgPSAiTW9kZWwgUGVyZm9ybWFuY2UiKQ0KDQoNCmRmX2NvbSAlPiUgDQogIGdyb3VwX2J5KE1vZGVsKSAlPiUgDQogIHN1bW1hcmlzZV9lYWNoKGZ1bnMobWVhbiksIEFjY3VyYWN5LCBBVUMsIExvZ2xvc3MsIFByZWNpc2lvbiwgUmVjYWxsLCBTcGVjaWZpY2l0eSkgJT4lIA0KICBhcnJhbmdlKC1BY2N1cmFjeSkgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgZnVuY3Rpb24oeCkge3JvdW5kKHgsIDMpfSkgJT4lIA0KICBrbml0cjo6a2FibGUoKQ0KDQpgYGANCg0KS+G6v3QgcXXhuqMgbsOgeSBjaOG7iSByYSBy4bqxbmc6IEF1dG9lbmNvZGVyIGPDsyB0aOG7gyDEkcaw4bujYyBz4butIGThu6VuZyBoaeG7h3UgcXXhuqMgbmjGsCBsw6AgbeG7mXQga8SpIHRodeG6rXQgY2hvIEZlYXR1cmUgRW5naW5lZXJpbmcuIA0KDQojIE9wdGltYWwgVGhyZXNob2xkDQoNClJGIHPhu60gZOG7pW5nIHRvw6BuIGLhu5kgNjEgYmnhur9uIHPhu5Egbmd1ecOqbiBi4bqjbiBsw6AgbcO0IGjDrG5oIGNobyBBY2N1cmFjeSBjYW8gbmjhuqV0LiBDaMO6bmcgdGEgY8OzIHRo4buDIGto4bqjbyBzw6F0IHPhu7EgYmnhur9uIMSR4buVaSBjw6FjIHRpw6p1IGNow60gxJHDoW5oIGdpw6EgY2jhuqV0IGzGsOG7o25nIG3DtCBow6xuaCBwaMOibiBsb+G6oWkga2hpIG5nxrDhu6FuZyDEkcaw4bujYyBs4buxYSBjaOG7jW4gZMOhbiBuaMOjbiAoQmFkIGhheSBHb29kKSB0aGF5IMSR4buVaSBuaMawIHNhdTogDQoNCmBgYHtyLCBmaWcuZnVsbHdpZHRoID0gVFJVRSwgZmlnLmhlaWdodD04LCBmaWcud2lkdGg9MTJ9DQpldmFsX2Z1biA8LSBmdW5jdGlvbih0aHJlKSB7DQogIGxhcHBseSgxOjEwLCBmdW5jdGlvbih4KSB7DQogICAgDQogICAgc2V0LnNlZWQoeCkNCiAgICBpZCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSBkZl90ZXN0JENsYXNzLCBwID0gMC41LCBsaXN0ID0gRkFMU0UpDQogICAgdGVzdF9kZiA8LSBkZl90ZXN0W2lkLCBdDQogIA0KICAgIGR1X2Jhb19wcm9iIDwtIGgyby5wcmVkaWN0KHB1cmVfbm4sIHRlc3RfZGYgJT4lIGFzLmgybygpKSAlPiUgDQogICAgICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICAgICAgcHVsbChCYWQpDQogICAgDQogICAgZHVfYmFvIDwtIGNhc2Vfd2hlbihkdV9iYW9fcHJvYiA+PSB0aHJlIH4gIkJhZCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgZHVfYmFvX3Byb2IgPCB0aHJlIH4gIkdvb2QiKSAlPiUgYXMuZmFjdG9yKCkNCiAgICBjbSA8LSBjb25mdXNpb25NYXRyaXgoZHVfYmFvLCB0ZXN0X2RmJENsYXNzLCBwb3NpdGl2ZSA9ICJCYWQiKQ0KICAgIA0KICAgIGJnX2dnIDwtIGNtJHRhYmxlICU+JSANCiAgICAgIGFzLnZlY3RvcigpICU+JSANCiAgICAgIG1hdHJpeChuY29sID0gNCkgJT4lIA0KICAgICAgYXMuZGF0YS5mcmFtZSgpICU+JSANCiAgICAgIHJlbmFtZShUUCA9IFYxLCBGTiA9IFYyLCBGUCA9IFYzLCBUTiA9IFY0KQ0KICANCiAgICANCiAgICBrcSA8LSBjKGNtJG92ZXJhbGwsIGNtJGJ5Q2xhc3MpIA0KICAgIHRlbiA8LSBrcSAlPiUgYXMuZGF0YS5mcmFtZSgpICU+JSByb3cubmFtZXMoKQ0KICAgIA0KICAgIGtxICU+JSANCiAgICAgIGFzLnZlY3RvcigpICU+JSANCiAgICAgIG1hdHJpeChuY29sID0gMTgpICU+JSANCiAgICAgIGFzLmRhdGEuZnJhbWUoKSAtPiBhbGxfZGYNCiAgICANCiAgICBuYW1lcyhhbGxfZGYpIDwtIHRlbg0KICAgIGFsbF9kZiA8LSBiaW5kX2NvbHMoYWxsX2RmLCBiZ19nZykNCiAgICByZXR1cm4oYWxsX2RmKQ0KICB9KQ0KfQ0KDQoNCg0KIyDEkMOhbmggZ2nDoSBz4buxIGJp4bq/biDEkeG7lWkgdGhlbyBt4buZdCBsb+G6oXQgbmfGsOG7oW5nOiANCg0Kc3lzdGVtLnRpbWUoc29fc2FuaF9saXN0IDwtIGxhcHBseShzZXEoMC4wNSwgMC44LCBieSA9IDAuMDUpLCBldmFsX2Z1bikpDQoNCnNvX3NhbmhfZGYgPC0gZG8uY2FsbCgiYmluZF9yb3dzIiwgc29fc2FuaF9saXN0KSANCg0Kc29fc2FuaF9kZiAlPD4lIA0KICBtdXRhdGUoVGhyZXNob2xkID0gbGFwcGx5KHNlcSgwLjA1LCAwLjgsIGJ5ID0gMC4wNSksIGZ1bmN0aW9uKHgpIHtyZXAoeCwgMTApfSkgJT4lIHVubGlzdCgpKQ0KDQp0aGVtZV9zZXQodGhlbWVfbWluaW1hbCgpKQ0Kc29fc2FuaF9kZiAlPiUgDQogIGdyb3VwX2J5KFRocmVzaG9sZCkgJT4lIA0KICBzdW1tYXJpc2VfZWFjaChmdW5zKG1lZGlhbiksIEFjY3VyYWN5LCBLYXBwYSwgU2Vuc2l0aXZpdHksIFNwZWNpZmljaXR5KSAlPiUgDQogIGdhdGhlcihNZXRyaWMsIGIsIC1UaHJlc2hvbGQpICU+JSANCiAgZ2dwbG90KGFlcyhUaHJlc2hvbGQsIGIsIGNvbG9yID0gTWV0cmljKSkgKyANCiAgZ2VvbV9saW5lKCkgKyANCiAgZ2VvbV9wb2ludChzaXplID0gMykgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKyANCiAgdGhlbWUocGFuZWwuZ3JpZC5taW5vciA9IGVsZW1lbnRfYmxhbmsoKSkgKyANCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLjA1LCAwLjgsIGJ5ID0gMC4wNSkpICsgDQogIGxhYnMoeSA9ICJBY2N1cmFjeSBSYXRlIiwgDQogICAgICAgdGl0bGUgPSAiVmFyaWF0aW9uIG9mIENsYXNzaWZpZXIncyBNZXRyaWNzIGJ5IFRocmVzaG9sZCIpDQpgYGANCg0KQ2jDum5nIHRhIGPDsyB0aOG7gyB0aOG6pXkgcsO1IHPhu7EgxJHDoW5oIMSR4buVaSDhu58gxJHDonk6IGto4bqzIG7Eg25nIHBow6JuIGxv4bqhaSBjaG8gbmjDo24gQmFkIGPDoG5nIGNhbyAoU2Vuc2l0aXZpdHkpIGPDoG5nIGNhbyB0aMOsIGto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSBjaG8gbmjDo24gR29vZCAoU3BlY2lmaWNpdHkpIGPDoG5nIHRo4bqlcC4gxJBp4buBdSBuw6B5IGThuqtuIMSR4bq/biBBY2N1cmFjeSBjw7MgZOG6oW5nIGjDrG5oIGNo4buvIFUgbmfGsOG7o2MgduG7m2kgbeG7qWMgY+G7sWMgxJHhuqFpIMSR4bqhdCDEkcaw4bujYyBraGkgbmfGsOG7oW5nIMSRxrDhu6NjIGNo4buNbiBsw6AgMC43LiANCg0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIGjDrG5oIOG6o25oIGjDs2Ega2jhuqMgbsSDbmcgcGjDom4gbG/huqFpIGPhu6dhIG3DtCBow6xuaCBjaG8gaGFpIGxv4bqhaSBo4buTIHPGoSBuaMawIHNhdTogDQoNCmBgYHtyLCBmaWcuZnVsbHdpZHRoID0gVFJVRSwgZmlnLmhlaWdodD04LCBmaWcud2lkdGg9MTJ9DQpteV9jbV9jb21fZGwgPC0gZnVuY3Rpb24odGhyZSkgew0KICBkdV9iYW9fcHJvYiA8LSBoMm8ucHJlZGljdChwdXJlX25uLCB0ZXN0KSAlPiUgYXMuZGF0YS5mcmFtZSgpICU+JSBwdWxsKEJhZCkNCiAgZHVfYmFvIDwtIGNhc2Vfd2hlbihkdV9iYW9fcHJvYiA+PSB0aHJlIH4gIkJhZCIsIA0KICAgICAgICAgICAgICAgICAgICAgIGR1X2Jhb19wcm9iIDwgdGhyZSB+ICJHb29kIikgJT4lIGFzLmZhY3RvcigpDQogIGNtIDwtIGNvbmZ1c2lvbk1hdHJpeChkdV9iYW8sIGRmX3Rlc3QkQ2xhc3MsIHBvc2l0aXZlID0gIkJhZCIpDQogIHJldHVybihjbSkNCiAgDQp9DQoNCm15X3RocmVzaG9sZCA8LSBjKDAuMTAsIDAuMjUsIDAuNSwgMC43KQ0KcmVzdWx0c19saXN0X2RsIDwtIGxhcHBseShteV90aHJlc2hvbGQsIG15X2NtX2NvbV9kbCkNCg0KDQp2aXNfZGV0ZWN0aW9uX3JhdGVfZGwgPC0gZnVuY3Rpb24oeCkgew0KICANCiAgcmVzdWx0c19saXN0X2RsW1t4XV0kdGFibGUgJT4lIGFzLmRhdGEuZnJhbWUoKSAtPiBtDQogIHJhdGUgPC0gcm91bmQoMTAwKm0kRnJlcVsxXSAvIHN1bShtJEZyZXFbYygxLCAyKV0pLCAyKQ0KICBhY2MgPC0gcm91bmQoMTAwKnN1bShtJEZyZXFbYygxLCA0KV0pIC8gc3VtKG0kRnJlcSksIDIpDQogIGFjYyA8LSBwYXN0ZTAoYWNjLCAiJSIpDQogIA0KICBtICU+JSANCiAgICBnZ3Bsb3QoYWVzKFJlZmVyZW5jZSwgRnJlcSwgZmlsbCA9IFByZWRpY3Rpb24pKSArDQogICAgZ2VvbV9jb2wocG9zaXRpb24gPSAiZmlsbCIpICsgDQogICAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzID0gYygiI2U0MWExYyIsICIjMzc3ZWI4IiksIG5hbWUgPSAiIikgKyANCiAgICB0aGVtZShwYW5lbC5ncmlkLm1pbm9yLnkgPSBlbGVtZW50X2JsYW5rKCkpICsgDQogICAgdGhlbWUocGFuZWwuZ3JpZC5taW5vci54ID0gZWxlbWVudF9ibGFuaygpKSArIA0KICAgIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsgDQogICAgbGFicyh4ID0gTlVMTCwgeSA9IE5VTEwsIA0KICAgICAgICAgdGl0bGUgPSBwYXN0ZTAoIkRldGVjdGluZyBCYWQgQ2FzZXMgd2hlbiBUaHJlc2hvbGQgPSAiLCBteV90aHJlc2hvbGRbeF0pLCANCiAgICAgICAgIHN1YnRpdGxlID0gcGFzdGUwKCJEZXRlY3RpbmcgUmF0ZSBmb3IgQmFkIENhc2VzOiAiLCByYXRlLCAiJSIsICIsICIsICJBY2N1cmFjeTogIiwgYWNjKSkNCiAgfQ0KDQoNCmdyaWRFeHRyYTo6Z3JpZC5hcnJhbmdlKHZpc19kZXRlY3Rpb25fcmF0ZV9kbCgxKSwgDQogICAgICAgICAgICAgICAgICAgICAgICB2aXNfZGV0ZWN0aW9uX3JhdGVfZGwoMiksIA0KICAgICAgICAgICAgICAgICAgICAgICAgdmlzX2RldGVjdGlvbl9yYXRlX2RsKDMpLCANCiAgICAgICAgICAgICAgICAgICAgICAgIHZpc19kZXRlY3Rpb25fcmF0ZV9kbCg0KSkNCg0KYGBgDQoNCg0KDQoNCg0KDQo=