
About Xgboost
Xgboost là một thuật toán Machine Learning được phát triển và hình thành từ một dự án của University of Washington bởi Tianqi Chen Carlos Guestrin. Ra đời khá muộn nhưng thuật toán này thường đạt thứ hạng cao trong các cuộc thi về phân tích dữ liệu trên Kaggle. Hiện tại thuật toán này đang có khoảng gần 400 contributors. Lịch sử ra đời của thuật toán này có thể tham khảo từ chính tác giả Tianqi Chen tại đây.
Là một open source có thể chạy trên cả hai hệ hiều hành Windown và Linux cho các ngôn ngữ phổ biến như C++, Java, Python, R, Julia, và Perl và có thể áp dụng cho một loạt các bài toán từ Regression, Classification đến các bài toán dự báo được định nghĩa riêng đặc trưng cho từng dự án (user-defined prediction) và cũng có thể thực thi trên các nền tảng cloud computing như AWS, Azure. Chi tiết về thuật toán này có thể tham khảo ở đây.
Conduct Xgboost in R
Thuật toán này có thể được thực hiện bằng một số thư viện như caret - một tool kit cho Machine Learning tương tự như scikit learn của Python hoặc trực tiếp sử dụng thư viện xgboost linh hoạt hơn và cho phép tinh chỉnh đầy đủ các tham số của thuật toán này và đây cũng là thư viện được sử dụng trong post này theo trình tự sau:
- Chuẩn bị dữ liệu cho Xgboost.
- Thực hiện huấn luyện Xgboost mặc định.
- Sử dụng Bayesian Optimization tinh chỉnh và tìm kiếm tham số tối ưu.
Dữ liệu sử dụng là hmeq.csv ở textbook Wiley and SAS Business: Credit Risk Analytics: Measurement Techniques, Applications, and Examples in SAS.
Data Preparation
Missing Data and Imputation
Các thuật toán Machine Learning đòi hỏi cả features lẫn target phải là đầy đủ. Với dữ liệu trống (missing) thì có thể có nhiều phương pháp thay thế dữ liệu trống (imputation) cho từng loại feature khác nhau. Trước hết chúng ta đánh giá mức độ thiếu của hai nhóm dữ liệu:
1 |
1100 |
25860 |
39025 |
HomeImp |
Other |
10.5 |
0 |
0 |
94.36667 |
1 |
9 |
NA |
1 |
1300 |
70053 |
68400 |
HomeImp |
Other |
7.0 |
0 |
2 |
121.83333 |
0 |
14 |
NA |
1 |
1500 |
13500 |
16700 |
HomeImp |
Other |
4.0 |
0 |
0 |
149.46667 |
1 |
10 |
NA |
1 |
1500 |
NA |
NA |
NA |
NA |
NA |
NA |
NA |
NA |
NA |
NA |
NA |
0 |
1700 |
97800 |
112000 |
HomeImp |
Office |
3.0 |
0 |
0 |
93.33333 |
0 |
14 |
NA |
1 |
1700 |
30548 |
40320 |
HomeImp |
Other |
9.0 |
0 |
0 |
101.46600 |
1 |
8 |
37.11361 |
## $REASON
## x
## DebtCon HomeImp <NA>
## 3928 1780 252
##
## $JOB
## x
## Mgr Office Other ProfExe Sales Self <NA>
## 767 948 2388 1276 109 193 279
## BAD LOAN MORTDUE VALUE YOJ DEROG DELINQ
## 0.00000000 0.00000000 0.08691275 0.01879195 0.08640940 0.11879195 0.09731544
## CLAGE NINQ CLNO DEBTINC
## 0.05167785 0.08557047 0.03724832 0.21258389
Biến REASON có 252 điểm dữ liệu missing. Còn với biến định lượng thì, ví dụ, biến DEBTINC thì tỉ lệ missing là 21.26%. Với biến định lượng (numeric) thì đơn giản nhất là thay thế bằng mean hoặc median. Với biến định tính (categorical) thì có thể thay thế các quan sát missing bằng các nhãn còn lại sao cho tỉ lệ (hay distribution) của cá nhãn thuộc biến được thay thế không thay đổi hoặc xấp xỉ tỉ lệ của các nhãn quan sát được trước khi thay thế. Phương pháp thay thế dữ liệu trống này là đơn giản nhất và sẽ không được sử dụng trong tình huống của bộ dữ liệu hmeq. Thay vì đó chúng ta thay thế dữ liệu trống bằng thuật toán Random Forest của thư viện missRanger như sau:
##
## Missing value imputation by random forests
##
## Variables to impute: MORTDUE, VALUE, REASON, JOB, YOJ, DEROG, DELINQ, CLAGE, NINQ, CLNO, DEBTINC
## Variables used to impute: BAD, LOAN, MORTDUE, VALUE, REASON, JOB, YOJ, DEROG, DELINQ, CLAGE, NINQ, CLNO, DEBTINC
## iter 1: ...........
## iter 2: ...........
## iter 3: ...........
Chúng ta có thể kiểm tra để xác nhận rằng các biến có missing data point đã được lấp đầy. Ví dụ với nhóm biến định tính:
## $REASON
## x
## DebtCon HomeImp
## 4101 1859
##
## $JOB
## x
## Mgr Office Other ProfExe Sales Self
## 784 990 2537 1337 119 193
Feature Engineering for Categorical Variables
Các thuận toán ML hầu hết (nếu không muốn nói là tất cả) đều đòi hỏi rằng các biến số sử dụng để huấn luyện mô hình phải là numeric nếu sử dụng Python. Với R thì yêu cầu này có thể không đòi hỏi. Tuy nhiên thuật toán Xgboost vẫn yêu cầu các biến phải là numeric cho cả R lẫn Python. Do vậy với biến categorical chúng ta có thể chuyển về numeric bằng một kĩ thuật gọi là one-hot encoding. Chúng ta có thể thực hiện kĩ thuật Feature Engineering này với hàm dummyVars() của thư viện caret như sau:
So sánh với dữ liệu ban đầu:
1 |
1100 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
1300 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
1500 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
1500 |
0 |
0 |
1 |
0 |
0 |
0 |
0 |
1700 |
0 |
1 |
0 |
0 |
0 |
0 |
1 |
1700 |
0 |
0 |
1 |
0 |
0 |
0 |
Đến đây chúng ta có thể sử dụng dữ liệu đã xử lí để huấn luyện Xgboost.
Train Xgboost
Thuật toán Xgboost đòi hỏi rằng dữ liệu không phải ở dạng data frame như thường thấy mà nên là DMatrix form bằng hàm xgb.DMatrix(). Trước hết phân chia dữ liệu ban đầu thành tỉ lệ 70 - 30 rồi chuyển về dạng DMatrix cho train data như sau:
Với train data đã ở dạng phù hợp với đòi hỏi của Xgboost chúng ta có thể huấn luyện thuật toán này mà không tinh chỉnh bất cứ tham số nào (default parameters) bằng hàm xgboost:
Để đánh giá chất lượng của mô hình trước hết chúng ta viết một hàm tính toán confusion matrix tương ứng với ngưỡng được chọn:
Khả năng dự báo của mô hình khi ngưỡng là 0.5 (đây là ngưỡng mặc định được lựa chọn cho phân loại cả ở R lẫn Python):
## Confusion Matrix and Statistics
##
## Reference
## Prediction Bad Good
## Bad 249 17
## Good 103 1419
##
## Accuracy : 0.9329
## 95% CI : (0.9203, 0.944)
## No Information Rate : 0.8031
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.7662
##
## Mcnemar's Test P-Value : 8.533e-15
##
## Sensitivity : 0.7074
## Specificity : 0.9882
## Pos Pred Value : 0.9361
## Neg Pred Value : 0.9323
## Prevalence : 0.1969
## Detection Rate : 0.1393
## Detection Prevalence : 0.1488
## Balanced Accuracy : 0.8478
##
## 'Positive' Class : Bad
##
Cũng có thể đánh giá vai trò - mức độ quan trọng của biến số trong việc phân loại (feature importance) bằng hàm xgb.importance() rồi hình ảnh hóa:

Hình dáng của đường ROC và giá trị AUC cũng là một tiêu chí được quan tâm hàng đầu của mô hình phân loại. Chúng ta có thể đánh chúng trên bộ dữ liệu test như sau:
# Use Xgboost for predicting:
pd <- predict(xgb1, X_test)
# Calculate AUC and plot ROC curve:
library(pROC)
my_auc <- roc(Y_test, pd)
sen_spec_df <- data_frame(TPR = my_auc$sensitivities,
FPR = 1 - my_auc$specificities,
total = my_auc$sensitivities + my_auc$specificities,
cutoff = my_auc$thresholds)
sen_spec_df %>%
ggplot(aes(x = FPR, ymin = 0, ymax = TPR))+
geom_polygon(aes(y = TPR), fill = "red", alpha = 0.3) +
geom_path(aes(y = TPR), col = "firebrick", size = 1.2) +
geom_abline(intercept = 0, slope = 1, color = "gray37", size = 1, linetype = "dashed") +
scale_y_continuous(labels = scales::percent) +
scale_x_continuous(labels = scales::percent) +
theme_bw() +
coord_equal() +
labs(x = "FPR (1 - Specificity)",
y = "TPR (Sensitivity)",
title = "Figure 2: Model Performance for Xgboost based on Test Data",
subtitle = paste0("AUC Value: ", my_auc$auc %>% round(4)))

Bayesian Optimization
Không tinh chỉnh bất cứ tham số nào cho Xgboost chúng ta cũng có được một mô hình phân loại với AUC = 0.9536. Khả năng phân loại của mô hình có thể sẽ tốt hơn nữa nếu chúng ta tinh chỉnh - tìm kiếm tham số tối ưu cho Xgboost. Hai cách thức tinh chỉnh phổ biến là:
Full Grid Search. Phương pháp này liệt kê tất cả những sự kết hợp khác nhau của tham số rồi thực hiện tinh chỉnh tương ứng. Cách thức này tốn rất nhiều thời gian và không hiệu quả - ít nhất là với trường hợp dữ liệu là lớn, nhiều biến số.
Random Search. Phương pháp này là một giải pháp cải tiến hơn so với Full Grid Search và sẽ mất ít thời gian hơn đi kèm với cái giá phải trả là phương án tốt nhất (hay bộ tham số tốt nhất) có thể bị bỏ qua. Về cơ chiến thuật này vẫn phải liệt kê ra tất cả các sự kết hợp của tham số và, ví dụ, quá trình tìm kiếm sẽ dừng lại nếu sau một số bước nào đó, model performance tính theo một tiêu chí chọn trước (như ROC-AUC chẳng hạn) không được cải thiện.
Bayesian Optimization. Phương pháp tinh chỉnh - tìm kiếm tham số tối ưu này hiệu quả hơn cả vì việc lụa chọn tham số cho bước kết tiếp sẽ được dựa vào thông tin về chất lượng của mô hình trước đó. Chi tiết về phương pháp này có thể tham khảo từ nhiều nguồn, ví dụ như ở đây.
Với R chúng ta có thể sử dụng thư viện rBayesianOptimization để thực hiện tinh chỉnh tham số cho Xgboost theo phương pháp này như sau:
- Thiết lập domain space - là không gian các tham số sẽ được tinh chỉnh. Bước này khá tương tự với thiết lập Full Grid Search.
- Viết hàm mục tiêu trả về giá trị của một tiêu chuẩn đánh giá mô hình mà chúng ta muốn tối ưu.
- Thực hiện tinh chỉnh tham số tối ưu bằng hàm BayesianOptimization().
Các bước chuẩn bị để tinh chỉnh - tìm kiếm tham số tối ưu cũng sẽ tương tự như vậy nếu thực hiện trong bất kì một ngôn ngữ nào khác. Nếu sử dụng Python thì có thể tham khảo ở đây.
Xgboost là mô hình có rất nhiều tham số để tinh chỉnh và có thể mất nhiều thời gian. Dưới đây là R codes thực hiện tinh chỉnh cho một số tham số được lựa chọn là max.depth, min_child_weight và subsample với các tham số còn lại thì sẽ chọn hoặc để mặc định:
# Objective function:
my_fun <- function(max.depth, min_child_weight, subsample) {
xgb <- xgboost(data = dtrain,
max_depth = max.depth,
min_child_weight = min_child_weight,
subsample = subsample,
objective = "binary:logistic",
verbose = 0,
nround = 30)
pd <- predict(xgb, X_test)
my_auc <- roc(Y_test, pd)$auc %>% as.numeric()
list(Score = my_auc, Pred = NULL)
}
# Domain space:
my_hypers <- list(max.depth = c(2L, 6L),
min_child_weight = c(1L, 10L),
subsample = c(0.5, 0.8))
# Search optimal hyperparameter by Bayesian Optimization:
library(rBayesianOptimization)
system.time(OPT_Res <- BayesianOptimization(my_fun,
bounds = my_hypers,
init_points = 10,
n_iter = 20,
acq = "ucb",
kappa = 2.576,
eps = 0.0,
verbose = FALSE))
##
## Best Parameters Found:
## Round = 23 max.depth = 6.0000 min_child_weight = 1.0000 subsample = 0.8000 Value = 0.9549
## user system elapsed
## 244.222 0.104 233.110
Mất chừng khoảng 200s trên con máy Dell Workstation T5610 chạy hai CPU Intel Xeon E5-2680 V2 để tinh chỉnh và tìm kiếm tham số tối ưu. Các tham số tối ưu và AUC tương ứng trên test data:
## max.depth min_child_weight subsample
## 6.0 1.0 0.8
## [1] 0.9548976
AUC trên train data tương ứng với các tham số tối ưu tìm được là 0.9575 cao hơn so với AUC = 0.9536 của Xgboost mặc định. Chúng ta vẫn còn có thể nâng cao chất lượng phân loại của mô hình bằng: (1) tỉnh chỉnh thêm một số tham số, (2) điều chỉnh lại domain space, hoặc (3) sự kết hợp nào đó của (1) và (2).
Brief Summary
Post này trình bày chi tiết các bước từ chuẩn bị dữ liệu (từ xử lí missing data + one-hot encoding cho categorical feature) cho đến bước tinh chỉnh tham số tối ưu theo Bayesian Optimization. Lưu ý rằng việc tinh chỉnh - tìm kiếm tham số tối ưu không hẳn chỉ là tìm tham số để tối đa hóa AUC như chúng ta vừa thực hiện mà có thể là các mục tiêu khác. Ví dụ: với một tổ chức hoạt động vì lợi nhuận như Ngân Hàng thì lợi nhuận là mục tiêu hàng đầu. Hơạc cũng chính Ngân Hàng đó nhưng trong bối cảnh bất ổn về kinh tế thì tiêu tham số tối ưu có thể phải hướng vào việc phân loại chính xác cao nhất các khách hàng xấu khi nộp hồ sơ xin vay hay cấp tín dụng (tức là Recall). Tinh chỉnh theo tham số hướng đến tối ưu các mục tiêu kinh tế này sẽ được trình bày trong một bài viết khác.
Về mặt kĩ thuật, thư viện rBayesianOptimization không hỗ trợ tính toán song song nên thời gian tinh chỉnh có thể lâu. Khắc phục nhược điểm này chúng ta có thể sử dụng thư viện ParBayesianOptimization để tăng tốc độ tìm kiếm tham số tối ưu.
References
- https://www.r-bloggers.com/bayesian-optimization-of-machine-learning-models/
- https://bearloga.github.io/bayesopt-tutorial-r/
- https://xgboost.readthedocs.io/en/latest/index.html
- https://papers.nips.cc/paper/4522-practical-bayesian-optimization-of-machine-learning-algorithms.pdf
- Snoek, J., Larochelle, H., & Adams, R. P. (2012). Practical bayesian optimization of machine learning algorithms. In Advances in neural information processing systems (pp. 2951-2959).
- Bergstra, J., Komer, B., Eliasmith, C., Yamins, D., & Cox, D. D. (2015). Hyperopt: a python library for model selection and hyperparameter optimization. Computational Science & Discovery, 8(1), 014008.
LS0tCnRpdGxlOiAnQmF5ZXNpYW4gT3B0aW1pemF0aW9uIGZvciBUdXJuaW5nIFhHQm9vc3QgSHlwZXJwYXJhbWV0ZXInCmF1dGhvcjogJ0F1dGhvcjogTmd1eWVuIENoaSBEdW5nJwpzdWJ0aXRsZTogIlIgTWFjaGluZSBMZWFybmluZyBTZXJpZXMiCm91dHB1dDoKICBodG1sX2RvY3VtZW50OiAKICAgIGNvZGVfZG93bmxvYWQ6IHRydWUKICAgICNjb2RlX2ZvbGRpbmc6IGhpZGUKICAgIGhpZ2hsaWdodDogemVuYnVybgogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcwogICAgdGhlbWU6ICJmbGF0bHkiCiAgICB0b2M6IFRSVUUKICAgIHRvY19mbG9hdDogVFJVRQotLS0KCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIGZpZy53aWR0aCA9IDEwLCBmaWcuaGVpZ2h0ID0gNikKYGBgCgohW10oL2hvbWUva2hhbmhhbi9Eb2N1bWVudHMveGdiLmpwZykKCgojIEFib3V0IFhnYm9vc3QKCgpYZ2Jvb3N0IGzDoCBt4buZdCB0aHXhuq10IHRvw6FuIE1hY2hpbmUgTGVhcm5pbmcgxJHGsOG7o2MgcGjDoXQgdHJp4buDbiB2w6AgaMOsbmggdGjDoG5oIHThu6sgbeG7mXQgZOG7sSDDoW4gY+G7p2EgVW5pdmVyc2l0eSBvZiBXYXNoaW5ndG9uIGLhu59pIFRpYW5xaSBDaGVuIENhcmxvcyBHdWVzdHJpbi4gUmEgxJHhu51pIGtow6EgbXXhu5luIG5oxrBuZyB0aHXhuq10IHRvw6FuIG7DoHkgdGjGsOG7nW5nIMSR4bqhdCB0aOG7qSBo4bqhbmcgY2FvIHRyb25nIGPDoWMgY3Xhu5ljIHRoaSB24buBIHBow6JuIHTDrWNoIGThu68gbGnhu4d1IHRyw6puIEthZ2dsZS4gSGnhu4duIHThuqFpIHRodeG6rXQgdG/DoW4gbsOgeSDEkWFuZyBjw7Mga2hv4bqjbmcgZ+G6p24gNDAwIGNvbnRyaWJ1dG9ycy4gTOG7i2NoIHPhu60gcmEgxJHhu51pIGPhu6dhIHRodeG6rXQgdG/DoW4gbsOgeSBjw7MgdGjhu4MgdGhhbSBraOG6o28gdOG7qyBjaMOtbmggdMOhYyBnaeG6oyBUaWFucWkgQ2hlbiBbdOG6oWkgxJHDonldKGh0dHBzOi8vaG9tZXMuY3Mud2FzaGluZ3Rvbi5lZHUvfnRxY2hlbi8yMDE2LzAzLzEwL3N0b3J5LWFuZC1sZXNzb25zLWJlaGluZC10aGUtZXZvbHV0aW9uLW9mLXhnYm9vc3QuaHRtbCkuIAoKCkzDoCBt4buZdCBvcGVuIHNvdXJjZSBjw7MgdGjhu4MgY2jhuqF5IHRyw6puIGPhuqMgaGFpIGjhu4cgaGnhu4F1IGjDoG5oIFdpbmRvd24gdsOgIExpbnV4IGNobyBjw6FjIG5nw7RuIG5n4buvIHBo4buVIGJp4bq/biBuaMawIEMrKywgSmF2YSwgUHl0aG9uLCBSLCBKdWxpYSwgdsOgIFBlcmwgdsOgIGPDsyB0aOG7gyDDoXAgZOG7pW5nIGNobyBt4buZdCBsb+G6oXQgY8OhYyBiw6BpIHRvw6FuIHThu6sgUmVncmVzc2lvbiwgQ2xhc3NpZmljYXRpb24gxJHhur9uIGPDoWMgYsOgaSB0b8OhbiBk4buxIGLDoW8gxJHGsOG7o2MgxJHhu4tuaCBuZ2jEqWEgcmnDqm5nIMSR4bq3YyB0csawbmcgY2hvIHThu6tuZyBk4buxIMOhbiAodXNlci1kZWZpbmVkIHByZWRpY3Rpb24pIHbDoCBjxaluZyBjw7MgdGjhu4MgdGjhu7FjIHRoaSB0csOqbiBjw6FjIG7hu4FuIHThuqNuZyBjbG91ZCBjb21wdXRpbmcgbmjGsCBBV1MsIEF6dXJlLiBDaGkgdGnhur90IHbhu4EgdGh14bqtdCB0b8OhbiBuw6B5IGPDsyB0aOG7gyB0aGFtIGto4bqjbyBb4bufIMSRw6J5XShodHRwczovL3hnYm9vc3QucmVhZHRoZWRvY3MuaW8vZW4vbGF0ZXN0L2J1aWxkLmh0bWwpLiAKCiMgQ29uZHVjdCBYZ2Jvb3N0IGluIFIKClRodeG6rXQgdG/DoW4gbsOgeSBjw7MgdGjhu4MgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiBi4bqxbmcgbeG7mXQgc+G7kSB0aMawIHZp4buHbiBuaMawIFtjYXJldF0oaHR0cDovL3RvcGVwby5naXRodWIuaW8vY2FyZXQvaW5kZXguaHRtbCkgLSBt4buZdCB0b29sIGtpdCBjaG8gTWFjaGluZSBMZWFybmluZyB0xrDGoW5nIHThu7EgbmjGsCBzY2lraXQgbGVhcm4gY+G7p2EgUHl0aG9uIGhv4bq3YyB0cuG7sWMgdGnhur9wIHPhu60gZOG7pW5nIHRoxrAgdmnhu4duIFt4Z2Jvb3N0XShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMveGdib29zdC92aWduZXR0ZXMveGdib29zdC5wZGYpIGxpbmggaG/huqF0IGjGoW4gdsOgIGNobyBwaMOpcCB0aW5oIGNo4buJbmggxJHhuqd5IMSR4bunIGPDoWMgdGhhbSBz4buRIGPhu6dhIHRodeG6rXQgdG/DoW4gbsOgeSB2w6AgxJHDonkgY8WpbmcgbMOgIHRoxrAgdmnhu4duIMSRxrDhu6NjIHPhu60gZOG7pW5nIHRyb25nIHBvc3QgbsOgeSB0aGVvIHRyw6xuaCB04buxIHNhdTogCgoxLiBDaHXhuqluIGLhu4sgZOG7ryBsaeG7h3UgY2hvIFhnYm9vc3QuIAoyLiBUaOG7sWMgaGnhu4duIGh14bqlbiBsdXnhu4duIFhnYm9vc3QgbeG6t2MgxJHhu4tuaC4gCjMuIFPhu60gZOG7pW5nIEJheWVzaWFuIE9wdGltaXphdGlvbiB0aW5oIGNo4buJbmggdsOgIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUuIAoKCkThu68gbGnhu4d1IHPhu60gZOG7pW5nIGzDoCAqKmhtZXEuY3N2Kiog4bufIHRleHRib29rIFtXaWxleSBhbmQgU0FTIEJ1c2luZXNzOiBDcmVkaXQgUmlzayBBbmFseXRpY3M6IE1lYXN1cmVtZW50IFRlY2huaXF1ZXMsIEFwcGxpY2F0aW9ucywgYW5kIEV4YW1wbGVzIGluIFNBU10oaHR0cDovL3d3dy5jcmVkaXRyaXNrYW5hbHl0aWNzLm5ldC8pLiAKCgojIERhdGEgUHJlcGFyYXRpb24KCiMjIE1pc3NpbmcgRGF0YSBhbmQgSW1wdXRhdGlvbgoKQ8OhYyB0aHXhuq10IHRvw6FuIE1hY2hpbmUgTGVhcm5pbmcgxJHDsmkgaOG7j2kgY+G6oyBmZWF0dXJlcyBs4bqrbiB0YXJnZXQgcGjhuqNpIGzDoCDEkeG6p3kgxJHhu6cuIFbhu5tpIGThu68gbGnhu4d1IHRy4buRbmcgKG1pc3NpbmcpIHRow6wgY8OzIHRo4buDIGPDsyBuaGnhu4F1IHBoxrDGoW5nIHBow6FwIHRoYXkgdGjhur8gZOG7ryBsaeG7h3UgdHLhu5FuZyAoaW1wdXRhdGlvbikgY2hvIHThu6tuZyBsb+G6oWkgZmVhdHVyZSBraMOhYyBuaGF1LiBUcsaw4bubYyBo4bq/dCBjaMO6bmcgdGEgxJHDoW5oIGdpw6EgbeG7qWMgxJHhu5kgdGhp4bq/dSBj4bunYSBoYWkgbmjDs20gZOG7ryBsaeG7h3U6IAoKCmBgYHtyfQojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgICAgICAgRGF0YSBQcmUtcHJvY2Vzc2luZwojPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CgojIExvYWQgc29tZSBwYWNrYWdlcyBmb3IgZGF0YSBtYW5pcHVsYXRpb246IApsaWJyYXJ5KHRpZHl2ZXJzZSkKCiMgQ2xlYXIgd29ya3NwYWNlOiAKcm0obGlzdCA9IGxzKCkpCgojIEltcG9ydCBkYXRhOiAKaG1lcSA8LSByZWFkX2NzdigiaHR0cDovL3d3dy5jcmVkaXRyaXNrYW5hbHl0aWNzLm5ldC91cGxvYWRzLzEvOS81LzEvMTk1MTE2MDEvaG1lcS5jc3YiKQoKIyBHbGltcHNlIG9yaWdpbmFsIGRhdGE6IApsaWJyYXJ5KGtuaXRyKQoKaG1lcSAlPiUgCiAgaGVhZCgpICU+JSAKICBrYWJsZSgpCgoKIyBDaGVjayBtaXNzaW5nIGRhdGEgZm9yIGNhdGVnb3JpY2FsIGZlYXR1cmVzOiAKc2FwcGx5KGhtZXEgJT4lIHNlbGVjdF9pZihpcy5jaGFyYWN0ZXIpLCBmdW5jdGlvbih4KSB7dGFibGUoeCwgdXNlTkEgPSAiaWZhbnkiKX0pCgojIENoZWNrIG1pc3NpbmcgcmF0ZSBmb3IgbnVtZXJpYyBmZWF0dXJlczogCnNhcHBseShobWVxICU+JSBzZWxlY3RfaWYoaXMubnVtZXJpYyksIGZ1bmN0aW9uKHgpIHtzdW0oaXMubmEoeCkpIC8gbGVuZ3RoKHgpfSkKCmBgYAoKQmnhur9uIFJFQVNPTiBjw7MgMjUyIMSRaeG7g20gZOG7ryBsaeG7h3UgbWlzc2luZy4gQ8OybiB24bubaSBiaeG6v24gxJHhu4tuaCBsxrDhu6NuZyB0aMOsLCB2w60gZOG7pSwgYmnhur9uIERFQlRJTkMgdGjDrCB04buJIGzhu4cgbWlzc2luZyBsw6AgMjEuMjYlLiBW4bubaSBiaeG6v24gxJHhu4tuaCBsxrDhu6NuZyAobnVtZXJpYykgdGjDrCDEkcahbiBnaeG6o24gbmjhuqV0IGzDoCB0aGF5IHRo4bq/IGLhurFuZyBtZWFuIGhv4bq3YyBtZWRpYW4uIFbhu5tpIGJp4bq/biDEkeG7i25oIHTDrW5oIChjYXRlZ29yaWNhbCkgdGjDrCBjw7MgdGjhu4MgdGhheSB0aOG6vyBjw6FjIHF1YW4gc8OhdCBtaXNzaW5nIGLhurFuZyBjw6FjIG5ow6NuIGPDsm4gbOG6oWkgc2FvIGNobyB04buJIGzhu4cgKGhheSBkaXN0cmlidXRpb24pIGPhu6dhIGPDoSBuaMOjbiB0aHXhu5ljIGJp4bq/biDEkcaw4bujYyB0aGF5IHRo4bq/IGtow7RuZyB0aGF5IMSR4buVaSBob+G6t2MgeOG6pXAgeOG7iSB04buJIGzhu4cgY+G7p2EgY8OhYyBuaMOjbiBxdWFuIHPDoXQgxJHGsOG7o2MgdHLGsOG7m2Mga2hpIHRoYXkgdGjhur8uIFBoxrDGoW5nIHBow6FwIHRoYXkgdGjhur8gZOG7ryBsaeG7h3UgdHLhu5FuZyBuw6B5IGzDoCDEkcahbiBnaeG6o24gbmjhuqV0IHbDoCBz4bq9IGtow7RuZyDEkcaw4bujYyBz4butIGThu6VuZyB0cm9uZyB0w6xuaCBodeG7kW5nIGPhu6dhIGLhu5kgZOG7ryBsaeG7h3UgaG1lcS4gVGhheSB2w6wgxJHDsyBjaMO6bmcgdGEgdGhheSB0aOG6vyBk4buvIGxp4buHdSB0cuG7kW5nIGLhurFuZyB0aHXhuq10IHRvw6FuIFJhbmRvbSBGb3Jlc3QgY+G7p2EgdGjGsCB2aeG7h24gKiptaXNzUmFuZ2VyKiogbmjGsCBzYXU6IAoKYGBge3J9CgojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiMgIFN0YWdlIDE6IEltcHV0YXRlIG1pc3NpbmcgZGF0YSBieSBSYW5kb20gRm9yZXN0CiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCiMgSW1wdXRlIG1pc3NpbmcgZGF0YTogCmxpYnJhcnkobWlzc1JhbmdlcikKCm1pc3NSYW5nZXIoaG1lcSwgc2VlZCA9IDI5KSAtPiBobWVxX2ltcHV0ZWQKCmBgYAoKQ2jDum5nIHRhIGPDsyB0aOG7gyBraeG7g20gdHJhIMSR4buDIHjDoWMgbmjhuq1uIHLhurFuZyBjw6FjIGJp4bq/biBjw7MgbWlzc2luZyBkYXRhIHBvaW50IMSRw6MgxJHGsOG7o2MgbOG6pXAgxJHhuqd5LiBWw60gZOG7pSB24bubaSBuaMOzbSBiaeG6v24gxJHhu4tuaCB0w61uaDogCgpgYGB7cn0KIyBDaGVjayBkYXRhIHNldCBhZnRlciBpbXB1dGluZzogCnNhcHBseShobWVxX2ltcHV0ZWQgJT4lIHNlbGVjdF9pZihpcy5jaGFyYWN0ZXIpLCBmdW5jdGlvbih4KSB7dGFibGUoeCwgdXNlTkEgPSAiaWZhbnkiKX0pCmBgYAoKCiMjIEZlYXR1cmUgRW5naW5lZXJpbmcgZm9yIENhdGVnb3JpY2FsIFZhcmlhYmxlcwoKQ8OhYyB0aHXhuq1uIHRvw6FuIE1MIGjhuqd1IGjhur90IChu4bq/dSBraMO0bmcgbXXhu5FuIG7Ds2kgbMOgIHThuqV0IGPhuqMpIMSR4buBdSDEkcOyaSBo4buPaSBy4bqxbmcgY8OhYyBiaeG6v24gc+G7kSBz4butIGThu6VuZyDEkeG7gyBodeG6pW4gbHV54buHbiBtw7QgaMOsbmggcGjhuqNpIGzDoCBudW1lcmljIG7hur91IHPhu60gZOG7pW5nIFB5dGhvbi4gVuG7m2kgUiB0aMOsIHnDqnUgY+G6p3UgbsOgeSBjw7MgdGjhu4Mga2jDtG5nIMSRw7JpIGjhu49pLiBUdXkgbmhpw6puIHRodeG6rXQgdG/DoW4gWGdib29zdCB24bqrbiB5w6p1IGPhuqd1IGPDoWMgYmnhur9uIHBo4bqjaSBsw6AgbnVtZXJpYyBjaG8gY+G6oyBSIGzhuqtuIFB5dGhvbi4gRG8gduG6rXkgduG7m2kgYmnhur9uIGNhdGVnb3JpY2FsIGNow7puZyB0YSBjw7MgdGjhu4MgY2h1eeG7g24gduG7gSBudW1lcmljIGLhurFuZyBt4buZdCBrxKkgdGh14bqtdCBn4buNaSBsw6AgW29uZS1ob3QgZW5jb2RpbmddKGh0dHBzOi8vbWFjaGluZWxlYXJuaW5nbWFzdGVyeS5jb20vd2h5LW9uZS1ob3QtZW5jb2RlLWRhdGEtaW4tbWFjaGluZS1sZWFybmluZy8pLiBDaMO6bmcgdGEgY8OzIHRo4buDIHRo4buxYyBoaeG7h24ga8SpIHRodeG6rXQgRmVhdHVyZSBFbmdpbmVlcmluZyBuw6B5IHbhu5tpIGjDoG0gKipkdW1teVZhcnMoKSoqIGPhu6dhIHRoxrAgdmnhu4duIGNhcmV0IG5oxrAgc2F1OiAKCmBgYHtyfQojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCiMgIFN0YWdlIDI6IENvbmR1Y3Qgb25lLWhvdCBlbmNvZGluZyBwcm9jZXNzCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCmxpYnJhcnkoY2FyZXQpCgojIENvbmR1Y3Qgb25lLWhvdCBlbmNvZGluZyBmb3IgY2F0ZWdvcmljYWwgZmVhdHVyZXM6IApkdW1taWVzIDwtIGR1bW15VmFycyh+IFJFQVNPTiArIEpPQiwgZGF0YSA9IGhtZXFfaW1wdXRlZCkKcHJlZGljdChkdW1taWVzLCBobWVxX2ltcHV0ZWQpICU+JSBhcy5kYXRhLmZyYW1lKCkgLT4gZmVhdHVyZXNfb25lSG90CgojIEZpbmFsIGRhdGEgZm9yIG1vZGVsbGluZzogCmhtZXFfaW1wdXRlZCAlPiUgCiAgc2VsZWN0X2lmKGlzLm51bWVyaWMpICU+JSAKICBiaW5kX2NvbHMoZmVhdHVyZXNfb25lSG90KSAtPiBkZl9maW5hbAoKYGBgCgpTbyBzw6FuaCB24bubaSBk4buvIGxp4buHdSBiYW4gxJHhuqd1OiAKCmBgYHtyfQoKIyBDb21wYXJlIHdpdGggb3JpZ2luYWwgZGF0YTogCmRmX2ZpbmFsICU+JSAKICBoZWFkKCkgJT4lIAogIHNlbGVjdChCQUQsIExPQU4sIGNvbnRhaW5zKCJKT0IiKSkgJT4lIAogIGthYmxlKCkKYGBgCgrEkOG6v24gxJHDonkgY2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyBk4buvIGxp4buHdSDEkcOjIHjhu60gbMOtIMSR4buDIGh14bqlbiBsdXnhu4duIFhnYm9vc3QuIAoKIyBUcmFpbiBYZ2Jvb3N0CgpUaHXhuq10IHRvw6FuIFhnYm9vc3QgxJHDsmkgaOG7j2kgcuG6sW5nIGThu68gbGnhu4d1IGtow7RuZyBwaOG6o2kg4bufIGThuqFuZyBkYXRhIGZyYW1lIG5oxrAgdGjGsOG7nW5nIHRo4bqleSBtw6AgbsOqbiBsw6AgRE1hdHJpeCBmb3JtIGLhurFuZyBow6BtICoqeGdiLkRNYXRyaXgoKSoqLiBUcsaw4bubYyBo4bq/dCBwaMOibiBjaGlhIGThu68gbGnhu4d1IGJhbiDEkeG6p3UgdGjDoG5oIHThu4kgbOG7hyA3MCAtIDMwIHLhu5NpIGNodXnhu4NuIHbhu4EgZOG6oW5nIERNYXRyaXggY2hvIHRyYWluIGRhdGEgbmjGsCBzYXU6IAoKYGBge3J9CiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09CiMgICBNb2RlbGxpbmcgd2l0aCBYR0Jvb3N0CiM9PT09PT09PT09PT09PT09PT09PT09PT09PT09CgojIFByZXBhcmUgZGF0YTogCgpzZXQuc2VlZCgxKQppZCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKGRmX2ZpbmFsJEJBRCwgcCA9IDAuNywgbGlzdCA9IEZBTFNFKQp0cmFpbiA8LSBkZl9maW5hbFtpZCwgXQp0ZXN0IDwtIGRmX2ZpbmFsWy1pZCwgXQoKCiMgQ29udmVydCBmZWF0dXJlcyB0byBETWF0cml4IGZvcm06IAoKWF90cmFpbiA8LSB0cmFpbiAlPiUgCiAgc2VsZWN0KC1CQUQpICU+JSAKICBhcy5tYXRyaXgoKQoKWV90cmFpbiA8LSB0cmFpbiAlPiUgCiAgcHVsbChCQUQpCgpYX3Rlc3QgPC0gdGVzdCAlPiUgCiAgc2VsZWN0KC1CQUQpICU+JSAKICBhcy5tYXRyaXgoKQoKWV90ZXN0IDwtIHRlc3QgJT4lIAogIHB1bGwoQkFEKQoKIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQojICAgVHJhaW4gWEdCb29zdCB3aXRoIGRlZmF1bHQgcGFyYW1ldGVycwojLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmxpYnJhcnkoeGdib29zdCkKCiMgQ29udmVydCB0byBETWF0cml4IGZvcm0gZm9yIHRyYWluIGRhdGE6IApkdHJhaW4gPC0geGdiLkRNYXRyaXgoZGF0YSA9IFhfdHJhaW4sIGxhYmVsID0gWV90cmFpbikKYGBgCgpW4bubaSB0cmFpbiBkYXRhIMSRw6Mg4bufIGThuqFuZyBwaMO5IGjhu6NwIHbhu5tpIMSRw7JpIGjhu49pIGPhu6dhIFhnYm9vc3QgY2jDum5nIHRhIGPDsyB0aOG7gyBodeG6pW4gbHV54buHbiB0aHXhuq10IHRvw6FuIG7DoHkgbcOgIGtow7RuZyB0aW5oIGNo4buJbmggYuG6pXQgY+G7qSB0aGFtIHPhu5EgbsOgbyAoZGVmYXVsdCBwYXJhbWV0ZXJzKSBi4bqxbmcgaMOgbSAqKnhnYm9vc3QqKjogCgoKYGBge3J9CiMgVHJhaW4gYSBkZWZhdWx0IFhHQm9vc3Q6IAp4Z2IxIDwtIHhnYm9vc3QoZGF0YSA9IGR0cmFpbiwgCiAgICAgICAgICAgICAgICBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIiwgCiAgICAgICAgICAgICAgICB2ZXJib3NlID0gMCwgCiAgICAgICAgICAgICAgICBucm91bmQgPSAzMCkKCmBgYAoKCsSQ4buDIMSRw6FuaCBnacOhIGNo4bqldCBsxrDhu6NuZyBj4bunYSBtw7QgaMOsbmggdHLGsOG7m2MgaOG6v3QgY2jDum5nIHRhIHZp4bq/dCBt4buZdCBow6BtIHTDrW5oIHRvw6FuIGNvbmZ1c2lvbiBtYXRyaXggdMawxqFuZyDhu6luZyB24bubaSBuZ8aw4buhbmcgxJHGsOG7o2MgY2jhu41uOiAKCmBgYHtyfQojIEZ1bmN0aW9uIGZvciBjYWxjdWxhdGVzIGNvbmZ1c2lvbiBtYXRyaXg6IAoKbXlfY20gPC0gZnVuY3Rpb24oeGdiX29iaiwgdGVzdF9kYXRhLCBuZ3VvbmcpIHsKICAKICBwcmVkaWN0aW9uIDwtIHByZWRpY3QoeGdiX29iaiwgdGVzdF9kYXRhKQogIAogIHByZWQgPC0gY2FzZV93aGVuKHByZWRpY3Rpb24gPj0gbmd1b25nIH4gIkJhZCIsIFRSVUUgfiAiR29vZCIpICU+JSBhcy5mYWN0b3IoKQogIAogIHRodWNfdGUgPC0gY2FzZV93aGVuKFlfdGVzdCA9PSAxIH4gIkJhZCIsIFlfdGVzdCA9PSAwIH4gIkdvb2QiKSAlPiUgYXMuZmFjdG9yKCkKICAKICBjb25mdXNpb25NYXRyaXgocHJlZCwgdGh1Y190ZSwgcG9zaXRpdmUgPSAiQmFkIikKfQoKYGBgCgpLaOG6oyBuxINuZyBk4buxIGLDoW8gY+G7p2EgbcO0IGjDrG5oIGtoaSBuZ8aw4buhbmcgbMOgIDAuNSAoxJHDonkgbMOgIG5nxrDhu6FuZyBt4bq3YyDEkeG7i25oIMSRxrDhu6NjIGzhu7FhIGNo4buNbiBjaG8gcGjDom4gbG/huqFpIGPhuqMg4bufIFIgbOG6q24gUHl0aG9uKTogCgpgYGB7cn0KIyBDb25mdXNpb24gbWF0cml4IHdpdGggY3V0b2ZmID0gMC41OiAKbXlfY20oeGdiMSwgWF90ZXN0LCAwLjUpCmBgYAoKQ8WpbmcgY8OzIHRo4buDIMSRw6FuaCBnacOhIHZhaSB0csOyIC0gbeG7qWMgxJHhu5kgcXVhbiB0cuG7jW5nIGPhu6dhIGJp4bq/biBz4buRIHRyb25nIHZp4buHYyBwaMOibiBsb+G6oWkgKGZlYXR1cmUgaW1wb3J0YW5jZSkgYuG6sW5nIGjDoG0gKip4Z2IuaW1wb3J0YW5jZSgpKiogcuG7k2kgaMOsbmgg4bqjbmggaMOzYTogCgpgYGB7cn0KCiMgRXh0cmFjdCBmZWF0dXJlIGltcG9ydGFuY2VzIGluIHRoZSBYR0Jvb3N0IHRyYWluZWQ6IAoKaW1wb3J0YW5jZSA8LSB4Z2IuaW1wb3J0YW5jZShmZWF0dXJlX25hbWVzID0gY29sbmFtZXMoWF90cmFpbiksIG1vZGVsID0geGdiMSkKCiMgUGxvdCBmZWF0dXJlIGltcG9ydGFuY2UgYnkgR2FpbjogCgoKbXlfY29sb3JzIDwtIGMoIiMwMTRkNjQiLCAiIzAxYTJkOSIpCgppbXBvcnRhbmNlICU+JSAKICBhcy5kYXRhLmZyYW1lKCkgJT4lIAogIG11dGF0ZShGZWF0dXJlID0gZmFjdG9yKEZlYXR1cmUsIEZlYXR1cmUpKSAlPiUgCiAgZ2dwbG90KGFlcyhGZWF0dXJlLCBHYWluKSkgKyAKICBnZW9tX2NvbChmaWxsID0gbXlfY29sb3JzWzJdKSArIAogIGNvb3JkX2ZsaXAoKSArIAogIHRoZW1lX21pbmltYWwoKSArIAogIGxhYnMoeCA9IE5VTEwsIHkgPSBOVUxMLCAKICAgICAgIHRpdGxlID0gIkZpZ3VyZSAxOiBGZWF0dXJlIEltcG9ydGFuY2UgYnkgR2FpbiBmb3IgWEdCb29zdCIsIAogICAgICAgc3VidGl0bGUgPSAiRGF0YSBTb3VyY2U6IGh0dHA6Ly93d3cuY3JlZGl0cmlza2FuYWx5dGljcy5uZXQiKQpgYGAKCkjDrG5oIGTDoW5nIGPhu6dhIMSRxrDhu51uZyBST0MgdsOgIGdpw6EgdHLhu4sgQVVDIGPFqW5nIGzDoCBt4buZdCB0acOqdSBjaMOtIMSRxrDhu6NjIHF1YW4gdMOibSBow6BuZyDEkeG6p3UgY+G7p2EgbcO0IGjDrG5oIHBow6JuIGxv4bqhaS4gQ2jDum5nIHRhIGPDsyB0aOG7gyDEkcOhbmggY2jDum5nIHRyw6puIGLhu5kgZOG7ryBsaeG7h3UgdGVzdCBuaMawIHNhdTogCgpgYGB7cn0KIyBVc2UgWGdib29zdCBmb3IgcHJlZGljdGluZzogCnBkIDwtIHByZWRpY3QoeGdiMSwgWF90ZXN0KQoKIyBDYWxjdWxhdGUgQVVDIGFuZCBwbG90IFJPQyBjdXJ2ZTogCmxpYnJhcnkocFJPQykKCm15X2F1YyA8LSByb2MoWV90ZXN0LCBwZCkKc2VuX3NwZWNfZGYgPC0gZGF0YV9mcmFtZShUUFIgPSBteV9hdWMkc2Vuc2l0aXZpdGllcywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgRlBSID0gMSAtIG15X2F1YyRzcGVjaWZpY2l0aWVzLCAKICAgICAgICAgICAgICAgICAgICAgICAgICB0b3RhbCA9IG15X2F1YyRzZW5zaXRpdml0aWVzICsgbXlfYXVjJHNwZWNpZmljaXRpZXMsIAogICAgICAgICAgICAgICAgICAgICAgICAgIGN1dG9mZiA9IG15X2F1YyR0aHJlc2hvbGRzKQoKCnNlbl9zcGVjX2RmICU+JSAKICBnZ3Bsb3QoYWVzKHggPSBGUFIsIHltaW4gPSAwLCB5bWF4ID0gVFBSKSkrCiAgZ2VvbV9wb2x5Z29uKGFlcyh5ID0gVFBSKSwgZmlsbCA9ICJyZWQiLCBhbHBoYSA9IDAuMykgKwogIGdlb21fcGF0aChhZXMoeSA9IFRQUiksIGNvbCA9ICJmaXJlYnJpY2siLCBzaXplID0gMS4yKSArCiAgZ2VvbV9hYmxpbmUoaW50ZXJjZXB0ID0gMCwgc2xvcGUgPSAxLCBjb2xvciA9ICJncmF5MzciLCBzaXplID0gMSwgbGluZXR5cGUgPSAiZGFzaGVkIikgKyAKICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSArCiAgc2NhbGVfeF9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKwogIHRoZW1lX2J3KCkgKwogIGNvb3JkX2VxdWFsKCkgKwogIGxhYnMoeCA9ICJGUFIgKDEgLSBTcGVjaWZpY2l0eSkiLCAKICAgICAgIHkgPSAiVFBSIChTZW5zaXRpdml0eSkiLCAKICAgICAgIHRpdGxlID0gIkZpZ3VyZSAyOiBNb2RlbCBQZXJmb3JtYW5jZSBmb3IgWGdib29zdCBiYXNlZCBvbiBUZXN0IERhdGEiLCAKICAgICAgIHN1YnRpdGxlID0gcGFzdGUwKCJBVUMgVmFsdWU6ICIsIG15X2F1YyRhdWMgJT4lIHJvdW5kKDQpKSkKCgpgYGAKCgojIEJheWVzaWFuIE9wdGltaXphdGlvbgoKS2jDtG5nIHRpbmggY2jhu4luaCBi4bqldCBj4bupIHRoYW0gc+G7kSBuw6BvIGNobyBYZ2Jvb3N0IGNow7puZyB0YSBjxaluZyBjw7MgxJHGsOG7o2MgbeG7mXQgbcO0IGjDrG5oIHBow6JuIGxv4bqhaSB24bubaSBBVUMgPSAwLjk1MzYuIEto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSBj4bunYSBtw7QgaMOsbmggY8OzIHRo4buDIHPhur0gdOG7kXQgaMahbiBu4buvYSBu4bq/dSBjaMO6bmcgdGEgdGluaCBjaOG7iW5oIC0gdMOsbSBraeG6v20gdGhhbSBz4buRIHThu5FpIMawdSBjaG8gWGdib29zdC4gSGFpIGPDoWNoIHRo4bupYyB0aW5oIGNo4buJbmggcGjhu5UgYmnhur9uIGzDoDogCgoxLiAqKkZ1bGwgR3JpZCBTZWFyY2gqKi4gUGjGsMahbmcgcGjDoXAgbsOgeSBsaeG7h3Qga8OqICp04bqldCBj4bqjKiBuaOG7r25nIHPhu7Ega+G6v3QgaOG7o3Aga2jDoWMgbmhhdSBj4bunYSB0aGFtIHPhu5EgcuG7k2kgdGjhu7FjIGhp4buHbiB0aW5oIGNo4buJbmggdMawxqFuZyDhu6luZy4gQ8OhY2ggdGjhu6ljIG7DoHkgdOG7kW4gcuG6pXQgbmhp4buBdSB0aOG7nWkgZ2lhbiB2w6Aga2jDtG5nIGhp4buHdSBxdeG6oyAtIMOtdCBuaOG6pXQgbMOgIHbhu5tpIHRyxrDhu51uZyBo4bujcCBk4buvIGxp4buHdSBsw6AgbOG7m24sIG5oaeG7gXUgYmnhur9uIHPhu5EuIAoKMi4gKipSYW5kb20gU2VhcmNoKiouIFBoxrDGoW5nIHBow6FwIG7DoHkgbMOgIG3hu5l0IGdp4bqjaSBwaMOhcCBj4bqjaSB0aeG6v24gaMahbiBzbyB24bubaSBGdWxsIEdyaWQgU2VhcmNoIHbDoCBz4bq9IG3huqV0IMOtdCB0aOG7nWkgZ2lhbiBoxqFuIMSRaSBrw6htIHbhu5tpIGPDoWkgZ2nDoSBwaOG6o2kgdHLhuqMgbMOgIHBoxrDGoW5nIMOhbiB04buRdCBuaOG6pXQgKGhheSBi4buZIHRoYW0gc+G7kSB04buRdCBuaOG6pXQpIGPDsyB0aOG7gyBi4buLIGLhu48gcXVhLiBW4buBIGPGoSBjaGnhur9uIHRodeG6rXQgbsOgeSB24bqrbiBwaOG6o2kgbGnhu4d0IGvDqiByYSB04bqldCBj4bqjIGPDoWMgc+G7sSBr4bq/dCBo4bujcCBj4bunYSB0aGFtIHPhu5EgdsOgLCB2w60gZOG7pSwgcXXDoSB0csOsbmggdMOsbSBraeG6v20gc+G6vSBk4burbmcgbOG6oWkgbuG6v3Ugc2F1IG3hu5l0IHPhu5EgYsaw4bubYyBuw6BvIMSRw7MsIG1vZGVsIHBlcmZvcm1hbmNlIHTDrW5oIHRoZW8gbeG7mXQgdGnDqnUgY2jDrSBjaOG7jW4gdHLGsOG7m2MgKG5oxrAgUk9DLUFVQyBjaOG6s25nIGjhuqFuKSBraMO0bmcgxJHGsOG7o2MgY+G6o2kgdGhp4buHbi4gCgozLiAqKkJheWVzaWFuIE9wdGltaXphdGlvbioqLiBQaMawxqFuZyBwaMOhcCB0aW5oIGNo4buJbmggLSB0w6xtIGtp4bq/bSB0aGFtIHPhu5EgdOG7kWkgxrB1IG7DoHkgaGnhu4d1IHF14bqjIGjGoW4gY+G6oyB2w6wgdmnhu4djIGzhu6VhIGNo4buNbiB0aGFtIHPhu5EgY2hvIGLGsOG7m2Mga+G6v3QgdGnhur9wIHPhur0gxJHGsOG7o2MgZOG7sWEgdsOgbyB0aMO0bmcgdGluIHbhu4EgY2jhuqV0IGzGsOG7o25nIGPhu6dhIG3DtCBow6xuaCB0csaw4bubYyDEkcOzLiBDaGkgdGnhur90IHbhu4EgcGjGsMahbmcgcGjDoXAgbsOgeSBjw7MgdGjhu4MgdGhhbSBraOG6o28gdOG7qyBuaGnhu4F1IG5ndeG7k24sIHbDrSBk4bulIG5oxrAgW+G7nyDEkcOieV0oaHR0cHM6Ly9naXRodWIuY29tL0Fub3RoZXJTYW1XaWxzb24vUGFyQmF5ZXNpYW5PcHRpbWl6YXRpb24pLgoKClbhu5tpIFIgY2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyB0aMawIHZp4buHbiAqckJheWVzaWFuT3B0aW1pemF0aW9uKiDEkeG7gyB0aOG7sWMgaGnhu4duIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgY2hvIFhnYm9vc3QgdGhlbyBwaMawxqFuZyBwaMOhcCBuw6B5IG5oxrAgc2F1OiAKCjEuIFRoaeG6v3QgbOG6rXAgZG9tYWluIHNwYWNlIC0gbMOgIGtow7RuZyBnaWFuIGPDoWMgdGhhbSBz4buRIHPhur0gxJHGsOG7o2MgdGluaCBjaOG7iW5oLiBCxrDhu5tjIG7DoHkga2jDoSB0xrDGoW5nIHThu7EgduG7m2kgdGhp4bq/dCBs4bqtcCBGdWxsIEdyaWQgU2VhcmNoLiAKMi4gVmnhur90IGjDoG0gbeG7pWMgdGnDqnUgdHLhuqMgduG7gSBnacOhIHRy4buLIGPhu6dhIG3hu5l0IHRpw6p1IGNodeG6qW4gxJHDoW5oIGdpw6EgbcO0IGjDrG5oIG3DoCBjaMO6bmcgdGEgbXXhu5FuIHThu5FpIMawdS4gCjMuIFRo4buxYyBoaeG7h24gdGluaCBjaOG7iW5oIHRoYW0gc+G7kSB04buRaSDGsHUgYuG6sW5nIGjDoG0gKipCYXllc2lhbk9wdGltaXphdGlvbigpKiouIAoKQ8OhYyBixrDhu5tjIGNodeG6qW4gYuG7iyDEkeG7gyB0aW5oIGNo4buJbmggLSB0w6xtIGtp4bq/bSB0aGFtIHPhu5EgdOG7kWkgxrB1IGPFqW5nIHPhur0gdMawxqFuZyB04buxIG5oxrAgduG6rXkgbuG6v3UgdGjhu7FjIGhp4buHbiB0cm9uZyBi4bqldCBrw6wgbeG7mXQgbmfDtG4gbmfhu68gbsOgbyBraMOhYy4gTuG6v3Ugc+G7rSBk4bulbmcgUHl0aG9uIHRow6wgY8OzIHRo4buDIHRoYW0ga2jhuqNvIFvhu58gxJHDonldKGh0dHBzOi8vZ2l0aHViLmNvbS9DaGlEdW5nTmd1eWVuL0NoYXB0ZXI2X0JheWVzaWFuX09wdGltaXphdGlvbi1SYW5kb21Gb3Jlc3QtL2Jsb2IvbWFzdGVyL0NoYXB0ZXI2X0JheWVzaWFuX09wdGltaXphdGlvbl9Gb3JfVHVybmluZ19QYXJhbWV0ZXJzLmlweW5iKS4gCgoKWGdib29zdCBsw6AgbcO0IGjDrG5oIGPDsyBy4bqldCBuaGnhu4F1IHRoYW0gc+G7kSDEkeG7gyB0aW5oIGNo4buJbmggdsOgIGPDsyB0aOG7gyBt4bqldCBuaGnhu4F1IHRo4budaSBnaWFuLiBExrDhu5tpIMSRw6J5IGzDoCBSIGNvZGVzIHRo4buxYyBoaeG7h24gdGluaCBjaOG7iW5oIGNobyBt4buZdCBz4buRIHRoYW0gc+G7kSDEkcaw4bujYyBs4buxYSBjaOG7jW4gbMOgICptYXguZGVwdGgqLCAqbWluX2NoaWxkX3dlaWdodCogdsOgICpzdWJzYW1wbGUqIHbhu5tpIGPDoWMgdGhhbSBz4buRIGPDsm4gbOG6oWkgdGjDrCBz4bq9IGNo4buNbiBob+G6t2MgxJHhu4MgbeG6t2MgxJHhu4tuaDogCgoKYGBge3J9CgojIE9iamVjdGl2ZSBmdW5jdGlvbjogCgpteV9mdW4gPC0gZnVuY3Rpb24obWF4LmRlcHRoLCBtaW5fY2hpbGRfd2VpZ2h0LCBzdWJzYW1wbGUpIHsKICAKICB4Z2IgPC0geGdib29zdChkYXRhID0gZHRyYWluLCAKICAgICAgICAgICAgICAgICBtYXhfZGVwdGggPSBtYXguZGVwdGgsCiAgICAgICAgICAgICAgICAgbWluX2NoaWxkX3dlaWdodCA9IG1pbl9jaGlsZF93ZWlnaHQsCiAgICAgICAgICAgICAgICAgc3Vic2FtcGxlID0gc3Vic2FtcGxlLCAKICAgICAgICAgICAgICAgICBvYmplY3RpdmUgPSAiYmluYXJ5OmxvZ2lzdGljIiwgCiAgICAgICAgICAgICAgICAgdmVyYm9zZSA9IDAsIAogICAgICAgICAgICAgICAgIG5yb3VuZCA9IDMwKQogIAogIHBkIDwtIHByZWRpY3QoeGdiLCBYX3Rlc3QpCiAgbXlfYXVjIDwtIHJvYyhZX3Rlc3QsIHBkKSRhdWMgJT4lIGFzLm51bWVyaWMoKQogIAogIGxpc3QoU2NvcmUgPSBteV9hdWMsIFByZWQgPSBOVUxMKQogIAogIAogIAp9CgoKIyBEb21haW4gc3BhY2U6IAoKbXlfaHlwZXJzIDwtIGxpc3QobWF4LmRlcHRoID0gYygyTCwgNkwpLAogICAgICAgICAgICAgICAgICBtaW5fY2hpbGRfd2VpZ2h0ID0gYygxTCwgMTBMKSwKICAgICAgICAgICAgICAgICAgc3Vic2FtcGxlID0gYygwLjUsIDAuOCkpCgoKCiMgU2VhcmNoIG9wdGltYWwgaHlwZXJwYXJhbWV0ZXIgYnkgQmF5ZXNpYW4gT3B0aW1pemF0aW9uOiAKbGlicmFyeShyQmF5ZXNpYW5PcHRpbWl6YXRpb24pCgpzeXN0ZW0udGltZShPUFRfUmVzIDwtIEJheWVzaWFuT3B0aW1pemF0aW9uKG15X2Z1biwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm91bmRzID0gbXlfaHlwZXJzLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBpbml0X3BvaW50cyA9IDEwLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX2l0ZXIgPSAyMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhY3EgPSAidWNiIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAga2FwcGEgPSAyLjU3NiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZXBzID0gMC4wLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZlcmJvc2UgPSBGQUxTRSkpCgpgYGAKCgpN4bqldCBjaOG7q25nIGtob+G6o25nIDIwMHMgdHLDqm4gY29uIG3DoXkgRGVsbCBXb3Jrc3RhdGlvbiBUNTYxMCBjaOG6oXkgaGFpIENQVSBJbnRlbCBYZW9uIEU1LTI2ODAgVjIgxJHhu4MgdGluaCBjaOG7iW5oIHbDoCB0w6xtIGtp4bq/bSB0aGFtIHPhu5EgdOG7kWkgxrB1LiBDw6FjIHRoYW0gc+G7kSB04buRaSDGsHUgdsOgIEFVQyB0xrDGoW5nIOG7qW5nIHRyw6puIHRlc3QgZGF0YTogCgpgYGB7cn0KIyBCZXN0IHBhcmFtZXRlcnM6IApPUFRfUmVzJEJlc3RfUGFyCiMgQmVzdCBBVUMgb24gdHJhaW4gZGF0YTogCk9QVF9SZXMkQmVzdF9WYWx1ZQpgYGAKCkFVQyB0csOqbiB0cmFpbiBkYXRhIHTGsMahbmcg4bupbmcgduG7m2kgY8OhYyB0aGFtIHPhu5EgdOG7kWkgxrB1IHTDrG0gxJHGsOG7o2MgbMOgIDAuOTU3NSBjYW8gaMahbiBzbyB24bubaSBBVUMgPSAwLjk1MzYgY+G7p2EgWGdib29zdCBt4bq3YyDEkeG7i25oLiBDaMO6bmcgdGEgduG6q24gY8OybiBjw7MgdGjhu4MgbsOibmcgY2FvIGNo4bqldCBsxrDhu6NuZyBwaMOibiBsb+G6oWkgY+G7p2EgbcO0IGjDrG5oIGLhurFuZzogKDEpIHThu4luaCBjaOG7iW5oIHRow6ptIG3hu5l0IHPhu5EgdGhhbSBz4buRLCAoMikgxJFp4buBdSBjaOG7iW5oIGzhuqFpIGRvbWFpbiBzcGFjZSwgaG/hurdjICgzKSBz4buxIGvhur90IGjhu6NwIG7DoG8gxJHDsyBj4bunYSAoMSkgdsOgICgyKS4gCgoKIyBCcmllZiBTdW1tYXJ5CgpQb3N0IG7DoHkgdHLDrG5oIGLDoHkgY2hpIHRp4bq/dCBjw6FjIGLGsOG7m2MgdOG7qyBjaHXhuqluIGLhu4sgZOG7ryBsaeG7h3UgKHThu6sgeOG7rSBsw60gbWlzc2luZyBkYXRhICsgb25lLWhvdCBlbmNvZGluZyBjaG8gY2F0ZWdvcmljYWwgZmVhdHVyZSkgY2hvIMSR4bq/biBixrDhu5tjIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgdOG7kWkgxrB1IHRoZW8gQmF5ZXNpYW4gT3B0aW1pemF0aW9uLiBMxrB1IMO9IHLhurFuZyB2aeG7h2MgdGluaCBjaOG7iW5oIC0gdMOsbSBraeG6v20gdGhhbSBz4buRIHThu5FpIMawdSBraMO0bmcgaOG6s24gY2jhu4kgbMOgIHTDrG0gdGhhbSBz4buRIMSR4buDIHThu5FpIMSRYSBow7NhIEFVQyBuaMawIGNow7puZyB0YSB24burYSB0aOG7sWMgaGnhu4duIG3DoCBjw7MgdGjhu4MgbMOgIGPDoWMgbeG7pWMgdGnDqnUga2jDoWMuIFbDrSBk4bulOiB24bubaSBt4buZdCB04buVIGNo4bupYyBob+G6oXQgxJHhu5luZyB2w6wgbOG7o2kgbmh14bqtbiBuaMawIE5nw6JuIEjDoG5nIHRow6wgbOG7o2kgbmh14bqtbiBsw6AgbeG7pWMgdGnDqnUgaMOgbmcgxJHhuqd1LiBIxqHhuqFjIGPFqW5nIGNow61uaCBOZ8OibiBIw6BuZyDEkcOzIG5oxrBuZyB0cm9uZyBi4buRaSBj4bqjbmggYuG6pXQg4buVbiB24buBIGtpbmggdOG6vyB0aMOsIHRpw6p1IHRoYW0gc+G7kSB04buRaSDGsHUgY8OzIHRo4buDIHBo4bqjaSBoxrDhu5tuZyB2w6BvIHZp4buHYyBwaMOibiBsb+G6oWkgY2jDrW5oIHjDoWMgY2FvIG5o4bqldCBjw6FjIGtow6FjaCBow6BuZyB44bqldSBraGkgbuG7mXAgaOG7kyBzxqEgeGluIHZheSBoYXkgY+G6pXAgdMOtbiBk4bulbmcgKHThu6ljIGzDoCBSZWNhbGwpLiBUaW5oIGNo4buJbmggdGhlbyB0aGFtIHPhu5EgaMaw4bubbmcgxJHhur9uIHThu5FpIMawdSBjw6FjIG3hu6VjIHRpw6p1IGtpbmggdOG6vyBuw6B5IHPhur0gxJHGsOG7o2MgdHLDrG5oIGLDoHkgdHJvbmcgbeG7mXQgYsOgaSB2aeG6v3Qga2jDoWMuIAoKVuG7gSBt4bq3dCBrxKkgdGh14bqtdCwgdGjGsCB2aeG7h24gckJheWVzaWFuT3B0aW1pemF0aW9uIGtow7RuZyBo4buXIHRy4bujIHTDrW5oIHRvw6FuIHNvbmcgc29uZyBuw6puIHRo4budaSBnaWFuIHRpbmggY2jhu4luaCBjw7MgdGjhu4MgbMOidS4gS2jhuq9jIHBo4bulYyBuaMaw4bujYyDEkWnhu4NtIG7DoHkgY2jDum5nIHRhIGPDsyB0aOG7gyBz4butIGThu6VuZyB0aMawIHZp4buHbiBbUGFyQmF5ZXNpYW5PcHRpbWl6YXRpb25dKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9QYXJCYXllc2lhbk9wdGltaXphdGlvbi9QYXJCYXllc2lhbk9wdGltaXphdGlvbi5wZGYpIMSR4buDIHTEg25nIHThu5FjIMSR4buZIHTDrG0ga2nhur9tIHRoYW0gc+G7kSB04buRaSDGsHUuIAoKIyBSZWZlcmVuY2VzCgoxLiBodHRwczovL3d3dy5yLWJsb2dnZXJzLmNvbS9iYXllc2lhbi1vcHRpbWl6YXRpb24tb2YtbWFjaGluZS1sZWFybmluZy1tb2RlbHMvCjIuIGh0dHBzOi8vYmVhcmxvZ2EuZ2l0aHViLmlvL2JheWVzb3B0LXR1dG9yaWFsLXIvCjMuIGh0dHBzOi8veGdib29zdC5yZWFkdGhlZG9jcy5pby9lbi9sYXRlc3QvaW5kZXguaHRtbAo0LiBodHRwczovL3BhcGVycy5uaXBzLmNjL3BhcGVyLzQ1MjItcHJhY3RpY2FsLWJheWVzaWFuLW9wdGltaXphdGlvbi1vZi1tYWNoaW5lLWxlYXJuaW5nLWFsZ29yaXRobXMucGRmCjUuIFNub2VrLCBKLiwgTGFyb2NoZWxsZSwgSC4sICYgQWRhbXMsIFIuIFAuICgyMDEyKS4gUHJhY3RpY2FsIGJheWVzaWFuIG9wdGltaXphdGlvbiBvZiBtYWNoaW5lIGxlYXJuaW5nIGFsZ29yaXRobXMuIEluIEFkdmFuY2VzIGluIG5ldXJhbCBpbmZvcm1hdGlvbiBwcm9jZXNzaW5nIHN5c3RlbXMgKHBwLiAyOTUxLTI5NTkpLgo2LiBCZXJnc3RyYSwgSi4sIEtvbWVyLCBCLiwgRWxpYXNtaXRoLCBDLiwgWWFtaW5zLCBELiwgJiBDb3gsIEQuIEQuICgyMDE1KS4gSHlwZXJvcHQ6IGEgcHl0aG9uIGxpYnJhcnkgZm9yIG1vZGVsIHNlbGVjdGlvbiBhbmQgaHlwZXJwYXJhbWV0ZXIgb3B0aW1pemF0aW9uLiBDb21wdXRhdGlvbmFsIFNjaWVuY2UgJiBEaXNjb3ZlcnksIDgoMSksIDAxNDAwOC4KCgpgYGB7ciwgZXZhbD1GQUxTRSwgZWNobz1GQUxTRX0KCm15X2Z1biA8LSBmdW5jdGlvbihtYXguZGVwdGgsIG1pbl9jaGlsZF93ZWlnaHQsIHN1YnNhbXBsZSkgewogIGN2IDwtIHhnYi5jdihwYXJhbXMgPSBsaXN0KG1heF9kZXB0aCA9IG1heC5kZXB0aCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fY2hpbGRfd2VpZ2h0ID0gbWluX2NoaWxkX3dlaWdodCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJzYW1wbGUgPSBzdWJzYW1wbGUsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9iamVjdGl2ZSA9ICJiaW5hcnk6bG9naXN0aWMiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGV2YWxfbWV0cmljID0gImF1YyIpLAogICAgICAgICAgICAgICBkYXRhID0gZHRyYWluLCAKICAgICAgICAgICAgICAgbnJvdW5kID0gMzAsCiAgICAgICAgICAgICAgIG5mb2xkID0gNSwgCiAgICAgICAgICAgICAgIHN0cmF0aWZpZWQgPSBUUlVFLCAKICAgICAgICAgICAgICAgcHJlZGljdGlvbiA9IFRSVUUsIAogICAgICAgICAgICAgICBzaG93c2QgPSBUUlVFLAogICAgICAgICAgICAgICBlYXJseV9zdG9wcGluZ19yb3VuZHMgPSAxMCwgCiAgICAgICAgICAgICAgIG1heGltaXplID0gVFJVRSwgCiAgICAgICAgICAgICAgIHZlcmJvc2UgPSAwKQogIAogIGxpc3QoU2NvcmUgPSBtYXgoY3YkZXZhbHVhdGlvbl9sb2ckdGVzdF9hdWNfbWVhbiksIFByZWQgPSBOVUxMKQp9CmBgYAoK