An Introduction to Ensemble Methods in Machine Learning

Ensemble Methods (tạm dịch là các phương pháp đồng diễn) là phương pháp kết hợp các mô hình dự báo khác nhau thành một mô hình dự báo có khả sức mạnh phân loại cao hơn ngay cả khi một hoặc một số mô hình thành phần có khả năng phân loại khiêm tốn hoặc yếu. Mô tả này rất giống với hình ảnh của “Năm anh em siêu nhân” mà có lẽ hầu hết thế hệ chúng ta đều xem:

Chi tiết hơn về cách tiếp cận này các bạn có thể tham khảo ở rất nhiều nguồn. Một số ví dụ:

  1. Nguồn 1.

  2. Nguồn 2

Trong Post này chúng ta sẽ tìm hiểu và khảo sát Ensemble Method với bộ số liệu hmeq.csv được sử dụng trong textbook điển hình về mô hình hóa rủi ro tín dụng Credit Risk Analytics: Measurement Techniques, Applications, and Examples in SAS. Bộ số liệu này có thể download từ: http://www.creditriskanalytics.net/uploads/1/9/5/1/19511601/hmeq.csv.

Trước hết chúng ta đọc dữ liệu và thực hiện một số thao tác xử lí sơ bộ và chuẩn bị số liệu:

Stage 1: Train Some Machine Learning Models

Trước hết chúng ta huấn luyện đồng thời 6 mô hình trong đó có một mô hình là hồi quy Logistic (đây không phải là một mô hình thuộc nhóm Machine Learning):

##    user  system elapsed 
##   43.17    1.13  517.27
Table 1: Model Performance in decreasing order of Accuracy
Model Average Accuracy
adaboost 0.907
rf 0.901
svmRadial 0.880
knn 0.859
glm 0.834
nb 0.822
Table 2: Model Performance in decreasing order of Sensitivity
Model Average Sensitivity
adaboost 0.673
rf 0.525
svmRadial 0.517
knn 0.367
glm 0.303
nb 0.100

Trong số 6 mô hình này thì adaboost có mức độ chính xác cao nhất và Logistic thì gần bét bảng. Tuy nhiên như đã nói ở đây, các tổ chức tài chính như ngân hàng sẽ không căn cứ vào Accuracy để chọn mô hình cho mục đích phân loại mà chú trọng nhiều hơn vào khả năng phân loại đúng nhãn hồ sơ xấu (có nhãn là Bad) - tức là tiêu chí Sensitivity. Trùng hợp là thứ hạng của Sensitivity của các mô hình là trùng hợp với thứ hạng của Accuracy như chúng ta có thể thấy ở bảng 2.

A Simple Combination for Constructing Ensemble Model

“Kết hợp” 6 mô hình ở trên chúng ta có thể tạo ra một Ensemble Model bằng hàm caretEnsemble() như sau:

## The following models were ensembled: rf, adaboost, knn, svmRadial, glm, nb 
## They were weighted: 
## -9.6919 10.6822 2.9071 3.2249 0.4164 -3.227 0.4381
## The resulting Accuracy is: 0.9016
## The fit for each individual model on the Accuracy is: 
##     method  Accuracy  AccuracySD
##         rf 0.9012548 0.005961170
##   adaboost 0.9067185 0.005153258
##        knn 0.8588940 0.010010165
##  svmRadial 0.8803611 0.006466756
##        glm 0.8342137 0.008004928
##         nb 0.8215031 0.009485290

Kết quả The resulting Accuracy is: 0.9016 nghĩa là Ensemble Model của chúng ta có Accuracy là 90.16% (chú ý đây là con số trung bình khi thử nghiệm trên 10 mẫu).

Chúng ta nên khảo sát kĩ hơn khả năng phân loại của mô hình Ensemble này và so sánh với các mô hình thành phần “cấu tạo” nên nó:

Table 3: Model Performance in decreasing order of Accuracy
Model Average Accuracy
adaboost 0.907
ensemble 0.902
rf 0.901
svmRadial 0.880
knn 0.859
glm 0.834
nb 0.822
Table 4: Model Performance in decreasing order of Sensitivity
Model Average Sensitivity
ensemble 0.929
adaboost 0.673
rf 0.525
svmRadial 0.517
knn 0.367
glm 0.303
nb 0.100

Mặc dù Ensemble Model có Accuracy mới chỉ xếp thứ 2 sau adaboost (bảng 3) nhưng đặc điểm sau mới là quan trọng (từ bảng 4): khả năng phân loại đúng các hồ sơ Bad tăng 38% từ 67.30% lên 92.9%.

Chúng ta cũng có thể sử dụng công cụ hình ảnh để đánh giá khả năng phân loại của các mô hình:

Khả năng phân loại chính xác hồ sơ Bad có thể được kiểm tra ngay trên test data. Dưới đây là so sánh Ensemble và mô hình “tốt nhất” adaboost:

## [[1]]
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  Bad Good
##       Bad   244   34
##       Good  112 1397
##                                           
##                Accuracy : 0.9183          
##                  95% CI : (0.9046, 0.9306)
##     No Information Rate : 0.8008          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.721           
##  Mcnemar's Test P-Value : 1.859e-10       
##                                           
##             Sensitivity : 0.6854          
##             Specificity : 0.9762          
##          Pos Pred Value : 0.8777          
##          Neg Pred Value : 0.9258          
##              Prevalence : 0.1992          
##          Detection Rate : 0.1365          
##    Detection Prevalence : 0.1556          
##       Balanced Accuracy : 0.8308          
##                                           
##        'Positive' Class : Bad             
##                                           
## 
## [[2]]
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  Bad Good
##       Bad   299   22
##       Good   57 1409
##                                           
##                Accuracy : 0.9558          
##                  95% CI : (0.9452, 0.9648)
##     No Information Rate : 0.8008          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.8561          
##  Mcnemar's Test P-Value : 0.0001306       
##                                           
##             Sensitivity : 0.8399          
##             Specificity : 0.9846          
##          Pos Pred Value : 0.9315          
##          Neg Pred Value : 0.9611          
##              Prevalence : 0.1992          
##          Detection Rate : 0.1673          
##    Detection Prevalence : 0.1796          
##       Balanced Accuracy : 0.9123          
##                                           
##        'Positive' Class : Bad             
## 

Ensemble Model phân loại chính xác 299 hồ sơ xấu (trong tổng số tất cả 396 hồ sơ xấu). Trong khi adaboost thì con số này chỉ là 244. Nguyên nhân có thể đến từ sự khác biệt của ROC / AUC. Chúng ta cũng có thể khảo sát ROC / AUC cho hai mô hình này:

## Area under the curve: 0.9869
## Area under the curve: 0.9706

A More Sophisticated Ensemble Model

Chúng ta có thể xây dựng một Ensemble Model phức tạp hơn với hàm caretStack(). Điểm cần chú ý là lúc này phải thiết lập một object control_new hoàn toàn mới:

Table 5: Model Performance in decreasing order of Accuracy
Model Average Accuracy
ensemble2 0.942
adaboost 0.907
ensemble 0.902
rf 0.901
svmRadial 0.880
knn 0.859
glm 0.834
nb 0.822
Table 6: Model Performance in decreasing order of Sensitivity
Model Average Sensitivity
ensemble 0.929
ensemble2 0.818
adaboost 0.673
rf 0.525
svmRadial 0.517
knn 0.367
glm 0.303
nb 0.100

Có thể thấy GBM Ensemble Model có Accuracy cao hơn đáng kể so với Ensemble đầu tiên (Linear Ensemble). Nhưng rất có thể GBM Ensemble Model chưa hẳn là mô hình phân loại tốt nhất vì Sensitivity - tức khả năng phân loại chính xác hồ sơ Bad thấp hơn cũng đáng kể. Chung ta nên kiểm tra trên test data để có đánh giá toàn diện hơn:

## Confusion Matrix and Statistics
## 
##           Reference
## Prediction  Bad Good
##       Bad   306   29
##       Good   50 1402
##                                           
##                Accuracy : 0.9558          
##                  95% CI : (0.9452, 0.9648)
##     No Information Rate : 0.8008          
##     P-Value [Acc > NIR] : < 2e-16         
##                                           
##                   Kappa : 0.8583          
##  Mcnemar's Test P-Value : 0.02444         
##                                           
##             Sensitivity : 0.8596          
##             Specificity : 0.9797          
##          Pos Pred Value : 0.9134          
##          Neg Pred Value : 0.9656          
##              Prevalence : 0.1992          
##          Detection Rate : 0.1712          
##    Detection Prevalence : 0.1875          
##       Balanced Accuracy : 0.9196          
##                                           
##        'Positive' Class : Bad             
## 

GBM Ensemble phân loại đúng 303 các hồ sơ Bad trong tổng số 356 trường hợp. Tức mức độ chính xác khi phân loại nhóm hồ sơ xấu là 85.11%. Đây là những dấu hiệu cho thấy GBM Ensemble là mô hình tốt nhất. Tuy nhiên để có kết luận chắc chắn hơn chúng ta cần đánh giá xa hơn ngay sau đây.

A Short Comparision between GBM and Linear Ensemble

Để so sánh toàn diện hơn GBM và Linear Ensemble chúng ta viết hai hàm tương tự nhau như về chức năng. Cụ thể, hàm sẽ đánh giá chừng 12 tiêu chí phân loại của mô hình dựa trên 15 lần chọn mẫu, mỗi một lần lấy 50% quan sát từ test data:

# Function 1: 

eval_fun_linear <- function(thre) {
  lapply(1:15, function(x) {
    
    set.seed(x)
    id <- createDataPartition(y = df_test_ml$BAD, p = 0.5, list = FALSE)
    test_df <- df_test_ml[id, ]
  
    du_bao_prob <- 1 - predict(greedy_ensemble, test_df, type = "prob")
    
    du_bao <- case_when(du_bao_prob >= thre ~ "Bad", 
                        du_bao_prob < thre ~ "Good") %>% as.factor()
    
    cm <- confusionMatrix(du_bao, test_df$BAD, 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)
  })
}



# Function 2: 

eval_fun_gbm <- function(thre) {
  lapply(1:15, function(x) {
    
    set.seed(x)
    id <- createDataPartition(y = df_test_ml$BAD, p = 0.5, list = FALSE)
    test_df <- df_test_ml[id, ]
  
    du_bao_prob <- 1 - predict(gbm_ensemble, test_df, type = "prob")
    
    du_bao <- case_when(du_bao_prob >= thre ~ "Bad", 
                        du_bao_prob < thre ~ "Good") %>% as.factor()
    
    cm <- confusionMatrix(du_bao, test_df$BAD, 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_list1 <- lapply(seq(0.05, 0.8, by = 0.05), eval_fun_linear))
##    user  system elapsed 
##  556.37    0.73  559.01
##    user  system elapsed 
##  561.78    0.67  563.89

Kết luận quan trọng là:

  1. Tại mọi ngưỡng thì ba trong bốn tiêu chí GBM Ensemble đều cao hơn.

  2. Accuracy là một đường cong bậc hai hình chữ U ngược. Nguyên nhân là do sự đánh đổi giữa hai thứ sau: khả năng phân loại hồ sơ tốt càng cao thì khả năng phân loại hồ sơ tốt sẽ giảm.

  3. Bắt đầu từ ngưỡng 0.35 thì các tiêu chí đánh giá khả năng phân loại của GBM Ensemble luôn cao hơn Linear Ensemble.

  4. Từ (1), (2) và (3) thì rõ ràng GBM Ensemble nên được các tổ chức tài chính lựa chọn như là mô hình phân loại và xếp hạng Credit Application.

LS0tDQp0aXRsZTogIkVuc2VtYmxlIExlYXJuaW5nIE1ldGhvZHM6IFdoYXQgQXJlIFRoZXkgYW5kIFdoeSBVc2UgVGhlbT8iIA0Kc3VidGl0bGU6ICJSIGZvciBLaWxsaW5nIFBuZXVtb25pYSINCmF1dGhvcjogIk5ndXllbiBDaGkgRHVuZyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgICMgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgaGlnaGxpZ2h0OiBweWdtZW50cw0KICAgICMgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImZsYXRseSINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCB3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSkNCmBgYA0KDQojIEFuIEludHJvZHVjdGlvbiB0byBFbnNlbWJsZSBNZXRob2RzIGluIE1hY2hpbmUgTGVhcm5pbmcNCg0KRW5zZW1ibGUgTWV0aG9kcyAodOG6oW0gZOG7i2NoIGzDoCAqKmPDoWMgcGjGsMahbmcgcGjDoXAgxJHhu5NuZyBkaeG7hW4qKikgbMOgIHBoxrDGoW5nIHBow6FwIGvhur90IGjhu6NwIGPDoWMgbcO0IGjDrG5oIGThu7EgYsOhbyBraMOhYyBuaGF1IHRow6BuaCBt4buZdCBtw7QgaMOsbmggZOG7sSBiw6FvIGPDsyBraOG6oyBz4bupYyBt4bqhbmggcGjDom4gbG/huqFpIGNhbyBoxqFuIG5nYXkgY+G6oyBraGkgbeG7mXQgaG/hurdjIG3hu5l0IHPhu5EgbcO0IGjDrG5oIHRow6BuaCBwaOG6p24gY8OzIGto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSBraGnDqm0gdOG7kW4gaG/hurdjIHnhur91LiBNw7QgdOG6oyBuw6B5IHLhuqV0IGdp4buRbmcgduG7m2kgaMOsbmgg4bqjbmggY+G7p2EgIk7Eg20gYW5oIGVtIHNpw6p1IG5ow6JuIiBtw6AgY8OzIGzhur0gaOG6p3UgaOG6v3QgdGjhur8gaOG7hyBjaMO6bmcgdGEgxJHhu4F1IHhlbTogDQoNCiFbXShDOi9Vc2Vycy9aYm9vay9Eb2N1bWVudHMvNV9hbmhfZW0uanBnKQ0KDQpDaGkgdGnhur90IGjGoW4gduG7gSBjw6FjaCB0aeG6v3AgY+G6rW4gbsOgeSBjw6FjIGLhuqFuIGPDsyB0aOG7gyB0aGFtIGto4bqjbyDhu58gcuG6pXQgbmhp4buBdSBuZ3Xhu5NuLiBN4buZdCBz4buRIHbDrSBk4bulOiANCg0KMS4gW05ndeG7k24gMV0oaHR0cHM6Ly9ibG9nLnN0YXRzYm90LmNvL2Vuc2VtYmxlLWxlYXJuaW5nLWQxZGNkNTQ4ZTkzNikuIA0KDQoyLiBbTmd14buTbiAyXShodHRwczovL21hY2hpbmVsZWFybmluZ21hc3RlcnkuY29tL21hY2hpbmUtbGVhcm5pbmctZW5zZW1ibGVzLXdpdGgtci8pDQoNClRyb25nIFBvc3QgbsOgeSBjaMO6bmcgdGEgc+G6vSB0w6xtIGhp4buDdSB2w6Aga2jhuqNvIHPDoXQgRW5zZW1ibGUgTWV0aG9kIHbhu5tpIGLhu5kgc+G7kSBsaeG7h3UgKipobWVxLmNzdioqIMSRxrDhu6NjIHPhu60gZOG7pW5nIHRyb25nIHRleHRib29rIMSRaeG7g24gaMOsbmggduG7gSBtw7QgaMOsbmggaMOzYSBy4bunaSBybyB0w61uIGThu6VuZyBbQ3JlZGl0IFJpc2sgQW5hbHl0aWNzOiBNZWFzdXJlbWVudCBUZWNobmlxdWVzLCBBcHBsaWNhdGlvbnMsIGFuZCBFeGFtcGxlcyBpbiBTQVNdKGh0dHBzOi8vd3d3LmFtYXpvbi5jb20vQ3JlZGl0LVJpc2stQW5hbHl0aWNzLU1lYXN1cmVtZW50LUFwcGxpY2F0aW9ucy9kcC8xMTE5MTQzOTg1KS4gQuG7mSBz4buRIGxp4buHdSBuw6B5IGPDsyB0aOG7gyBkb3dubG9hZCB04burOiBodHRwOi8vd3d3LmNyZWRpdHJpc2thbmFseXRpY3MubmV0L3VwbG9hZHMvMS85LzUvMS8xOTUxMTYwMS9obWVxLmNzdi4gDQoNCg0KVHLGsOG7m2MgaOG6v3QgY2jDum5nIHRhIMSR4buNYyBk4buvIGxp4buHdSB2w6AgdGjhu7FjIGhp4buHbiBt4buZdCBz4buRIHRoYW8gdMOhYyB44butIGzDrSBzxqEgYuG7mSB2w6AgY2h14bqpbiBi4buLIHPhu5EgbGnhu4d1OiANCg0KYGBge3IsIGZpZy5mdWxsd2lkdGggPSBUUlVFLCBmaWcuaGVpZ2h0PTcsIGZpZy53aWR0aD0xMn0NCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCiMgUGVyZm9ybSBzb21lIGRhdGEgcHJlLXByb2Nlc3NpbmcNCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0NCg0KIyBMb2FkIHNvbWUgcGFja2FnZXM6IA0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KG1hZ3JpdHRyKQ0KDQojIEltcG9ydCBkYXRhOiANCmhtZXEgPC0gcmVhZC5jc3YoImh0dHA6Ly93d3cuY3JlZGl0cmlza2FuYWx5dGljcy5uZXQvdXBsb2Fkcy8xLzkvNS8xLzE5NTExNjAxL2htZXEuY3N2IikNCg0KIyBGdW5jdGlvbiByZXBsYWNlcyBOQSBieSBtZWFuOiANCnJlcGxhY2VfYnlfbWVhbiA8LSBmdW5jdGlvbih4KSB7DQogIHhbaXMubmEoeCldIDwtIG1lYW4oeCwgbmEucm0gPSBUUlVFKQ0KICByZXR1cm4oeCkNCn0NCg0KIyBBIGZ1bmN0aW9uIGltcHV0ZXMgTkEgb2JzZXJ2YXRpb25zIGZvciBjYXRlZ29yaWNhbCB2YXJpYWJsZXM6IA0KDQpyZXBsYWNlX25hX2NhdGVnb3JpY2FsIDwtIGZ1bmN0aW9uKHgpIHsNCiAgeCAlPiUgDQogICAgdGFibGUoKSAlPiUgDQogICAgYXMuZGF0YS5mcmFtZSgpICU+JSANCiAgICBhcnJhbmdlKC1GcmVxKSAtPj4gbXlfZGYNCiAgDQogIG5fb2JzIDwtIHN1bShteV9kZiRGcmVxKQ0KICBwb3AgPC0gbXlfZGYkLiAlPiUgYXMuY2hhcmFjdGVyKCkNCiAgc2V0LnNlZWQoMjkpDQogIHhbaXMubmEoeCldIDwtIHNhbXBsZShwb3AsIHN1bShpcy5uYSh4KSksIHJlcGxhY2UgPSBUUlVFLCBwcm9iID0gbXlfZGYkRnJlcSkNCiAgcmV0dXJuKHgpDQp9DQoNCiMgVXNlIHRoZSB0d28gZnVuY3Rpb25zOiANCmRmIDwtIGhtZXEgJT4lIA0KICBtdXRhdGVfaWYoaXMuZmFjdG9yLCBhcy5jaGFyYWN0ZXIpICU+JSANCiAgbXV0YXRlKFJFQVNPTiA9IGNhc2Vfd2hlbihSRUFTT04gPT0gIiIgfiBOQV9jaGFyYWN0ZXJfLCBUUlVFIH4gUkVBU09OKSwgDQogICAgICAgICBKT0IgPSBjYXNlX3doZW4oSk9CID09ICIiIH4gTkFfY2hhcmFjdGVyXywgVFJVRSB+IEpPQikpICU+JQ0KICBtdXRhdGVfaWYoaXNfY2hhcmFjdGVyLCBhcy5mYWN0b3IpICU+JSANCiAgbXV0YXRlX2lmKGlzLm51bWVyaWMsIHJlcGxhY2VfYnlfbWVhbikgJT4lIA0KICBtdXRhdGVfaWYoaXMuZmFjdG9yLCByZXBsYWNlX25hX2NhdGVnb3JpY2FsKQ0KDQoNCiMgQ29udmVydCBCQUQgdG8gZmFjdG9yIGFuZCBzY2FsZSAwIC0xIGRhdGEgc2V0OiANCmRmX2Zvcl9tbCA8LSBkZiAlPiUgDQogIG11dGF0ZShCQUQgPSBjYXNlX3doZW4oQkFEID09IDEgfiAiQmFkIiwgVFJVRSB+ICJHb29kIikgJT4lIGFzLmZhY3RvcigpKSAlPiUgDQogIG11dGF0ZV9pZihpcy5udW1lcmljLCBmdW5jdGlvbih4KSB7KHggLSBtaW4oeCkpIC8gKG1heCh4KSAtIG1pbih4KSl9KQ0KYGBgDQoNCiMgU3RhZ2UgMTogVHJhaW4gU29tZSBNYWNoaW5lIExlYXJuaW5nIE1vZGVscw0KDQpUcsaw4bubYyBo4bq/dCBjaMO6bmcgdGEgaHXhuqVuIGx1eeG7h24gxJHhu5NuZyB0aOG7nWkgNiBtw7QgaMOsbmggdHJvbmcgxJHDsyBjw7MgbeG7mXQgbcO0IGjDrG5oIGzDoCBo4buTaSBxdXkgTG9naXN0aWMgKMSRw6J5IGtow7RuZyBwaOG6o2kgbMOgIG3hu5l0IG3DtCBow6xuaCB0aHXhu5ljIG5ow7NtIE1hY2hpbmUgTGVhcm5pbmcpOiANCg0KYGBge3IsIGZpZy5mdWxsd2lkdGggPSBUUlVFLCBmaWcuaGVpZ2h0PTcsIGZpZy53aWR0aD0xMn0NCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KIyAgU2ltdWx0YW5lb3VzbHkgVHJhaW4gNSBNb2RlbHMNCiMtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KDQojIFNwbGl0IGRhdGE6IA0KbGlicmFyeShjYXJldCkNCnNldC5zZWVkKDEpDQppZCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSBkZl9mb3JfbWwkQkFELCBwID0gMC43LCBsaXN0ID0gRkFMU0UpDQpkZl90cmFpbl9tbCA8LSBkZl9mb3JfbWxbaWQsIF0NCmRmX3Rlc3RfbWwgPC0gZGZfZm9yX21sWy1pZCwgXQ0KDQojIFNldCBjb25kaXRpb25zIGZvciB0cmFpbmluZyBtb2RlbCBhbmQgY3Jvc3MtdmFsaWRhdGlvbjogDQoNCnNldC5zZWVkKDEpDQpudW1iZXIgPC0gNQ0KcmVwZWF0cyA8LSAyDQpjb250cm9sIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgbnVtYmVyID0gbnVtYmVyICwgDQogICAgICAgICAgICAgICAgICAgICAgICByZXBlYXRzID0gcmVwZWF0cywgDQogICAgICAgICAgICAgICAgICAgICAgICBjbGFzc1Byb2JzID0gVFJVRSwgDQogICAgICAgICAgICAgICAgICAgICAgICBzYXZlUHJlZGljdGlvbnMgPSAiZmluYWwiLCANCiAgICAgICAgICAgICAgICAgICAgICAgIGluZGV4ID0gY3JlYXRlUmVzYW1wbGUoZGZfdHJhaW5fbWwkQkFELCByZXBlYXRzKm51bWJlciksIA0KICAgICAgICAgICAgICAgICAgICAgICAgc3VtbWFyeUZ1bmN0aW9uID0gbXVsdGlDbGFzc1N1bW1hcnksIA0KICAgICAgICAgICAgICAgICAgICAgICAgYWxsb3dQYXJhbGxlbCA9IFRSVUUpDQoNCiMgVXNlIFBhcmFsbGVsIGNvbXB1dGluZzogDQpsaWJyYXJ5KGRvUGFyYWxsZWwpDQpyZWdpc3RlckRvUGFyYWxsZWwoY29yZXMgPSBkZXRlY3RDb3JlcygpIC0gMSkNCg0KIyA2IG1vZGVscyBzZWxlY3RlZDogDQoNCm15X21vZGVscyA8LSBjKCJyZiIsICJhZGFib29zdCIsICJrbm4iLCAic3ZtUmFkaWFsIiwgImdsbSIsICJuYiIpDQoNCiMgVHJhaW4gdGhlc2UgTUwgTW9kZWxzOiANCmxpYnJhcnkoY2FyZXRFbnNlbWJsZSkNCnNldC5zZWVkKDEpDQpzeXN0ZW0udGltZShtb2RlbF9saXN0MSA8LSBjYXJldExpc3QoQkFEIH4uLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gZGZfdHJhaW5fbWwsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gY29udHJvbCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRyaWMgPSAiQWNjdXJhY3kiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2RMaXN0ID0gbXlfbW9kZWxzKSkNCg0KDQpsaXN0X29mX3Jlc3VsdHMgPC0gbGFwcGx5KG15X21vZGVscywgZnVuY3Rpb24oeCkge21vZGVsX2xpc3QxW1t4XV0kcmVzYW1wbGV9KQ0KDQojIENvbnZlcnQgdG8gZGF0YSBmcmFtZTogDQp0b3RhbF9kZiA8LSBkby5jYWxsKCJiaW5kX3Jvd3MiLCBsaXN0X29mX3Jlc3VsdHMpDQp0b3RhbF9kZiAlPD4lIG11dGF0ZShNb2RlbCA9IGxhcHBseShteV9tb2RlbHMsIGZ1bmN0aW9uKHgpIHtyZXAoeCwgbnVtYmVyKnJlcGVhdHMpfSkgJT4lIHVubGlzdCgpKQ0KDQojIEF2ZXJhZ2UgQWNjdXJhY3kgYmFzZWQgb24gMTAgc2FtcGxlcyBmb3IgdGhlc2UgbW9kZWxzOiANCnRvdGFsX2RmICU+JSANCiAgZ3JvdXBfYnkoTW9kZWwpICU+JSANCiAgc3VtbWFyaXNlKGF2Z19hY2MgPSBtZWFuKEFjY3VyYWN5KSkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBhcnJhbmdlKC1hdmdfYWNjKSAlPiUgDQogIG11dGF0ZV9pZihpcy5udW1lcmljLCBmdW5jdGlvbih4KSB7cm91bmQoeCwgMyl9KSAlPiUgDQogIGtuaXRyOjprYWJsZShjYXB0aW9uID0gIlRhYmxlIDE6IE1vZGVsIFBlcmZvcm1hbmNlIGluIGRlY3JlYXNpbmcgb3JkZXIgb2YgQWNjdXJhY3kiLCANCiAgICAgICAgICAgICAgIGNvbC5uYW1lcyA9IGMoIk1vZGVsIiwgIkF2ZXJhZ2UgQWNjdXJhY3kiKSkNCg0KDQojIEF2ZXJhZ2UgU2Vuc2l0aXZpdHkgYmFzZWQgb24gMTAgc2FtcGxlcyBmb3IgdGhlc2UgbW9kZWxzOiANCnRvdGFsX2RmICU+JSANCiAgZ3JvdXBfYnkoTW9kZWwpICU+JSANCiAgc3VtbWFyaXNlKGF2Z19zZW4gPSBtZWFuKFNlbnNpdGl2aXR5KSkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBhcnJhbmdlKC1hdmdfc2VuKSAlPiUgDQogIG11dGF0ZV9pZihpcy5udW1lcmljLCBmdW5jdGlvbih4KSB7cm91bmQoeCwgMyl9KSAlPiUgDQogIGtuaXRyOjprYWJsZShjYXB0aW9uID0gIlRhYmxlIDI6IE1vZGVsIFBlcmZvcm1hbmNlIGluIGRlY3JlYXNpbmcgb3JkZXIgb2YgU2Vuc2l0aXZpdHkiLCANCiAgICAgICAgICAgICAgIGNvbC5uYW1lcyA9IGMoIk1vZGVsIiwgIkF2ZXJhZ2UgU2Vuc2l0aXZpdHkiKSkNCmBgYA0KDQpUcm9uZyBz4buRIDYgbcO0IGjDrG5oIG7DoHkgdGjDrCBhZGFib29zdCBjw7MgbeG7qWMgxJHhu5kgY2jDrW5oIHjDoWMgY2FvIG5o4bqldCB2w6AgTG9naXN0aWMgdGjDrCBn4bqnbiBiw6l0IGLhuqNuZy4gVHV5IG5oacOqbiBuaMawIMSRw6MgbsOzaSBb4bufIMSRw6J5XShodHRwOi8vcnB1YnMuY29tL2NoaWR1bmdrdC80NDc5ODkpLCBjw6FjIHThu5UgY2jhu6ljIHTDoGkgY2jDrW5oIG5oxrAgbmfDom4gaMOgbmcgc+G6vSBraMO0bmcgY8SDbiBj4bupIHbDoG8gQWNjdXJhY3kgxJHhu4MgY2jhu41uIG3DtCBow6xuaCBjaG8gbeG7pWMgxJHDrWNoIHBow6JuIGxv4bqhaSBtw6AgY2jDuiB0cuG7jW5nIG5oaeG7gXUgaMahbiB2w6BvIGto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSDEkcO6bmcgbmjDo24gaOG7kyBzxqEgeOG6pXUgKGPDsyBuaMOjbiBsw6AgQmFkKSAtIHThu6ljIGzDoCB0acOqdSBjaMOtIFNlbnNpdGl2aXR5LiBUcsO5bmcgaOG7o3AgbMOgIHRo4bupIGjhuqFuZyBj4bunYSBTZW5zaXRpdml0eSBj4bunYSBjw6FjIG3DtCBow6xuaCBsw6AgdHLDuW5nIGjhu6NwIHbhu5tpIHRo4bupIGjhuqFuZyBj4bunYSBBY2N1cmFjeSBuaMawIGNow7puZyB0YSBjw7MgdGjhu4MgdGjhuqV5IOG7nyBi4bqjbmcgMi4gDQoNCiMgQSBTaW1wbGUgQ29tYmluYXRpb24gZm9yIENvbnN0cnVjdGluZyBFbnNlbWJsZSBNb2RlbA0KDQoiS+G6v3QgaOG7o3AiIDYgbcO0IGjDrG5oIOG7nyB0csOqbiBjaMO6bmcgdGEgY8OzIHRo4buDIHThuqFvIHJhIG3hu5l0IEVuc2VtYmxlIE1vZGVsIGLhurFuZyBow6BtICoqY2FyZXRFbnNlbWJsZSgpKiogbmjGsCBzYXU6IA0KDQpgYGB7ciwgZmlnLmZ1bGx3aWR0aCA9IFRSVUUsIGZpZy5oZWlnaHQ9NywgZmlnLndpZHRoPTEyfQ0KIyBDb21iaW5lIDYgbW9kZWxzOiANCmdyZWVkeV9lbnNlbWJsZSA8LSBjYXJldEVuc2VtYmxlKG1vZGVsX2xpc3QxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0ckNvbnRyb2wgPSBjb250cm9sKQ0KDQojIERyYWZ0IFJlc3VsdHM6IA0Kc3VtbWFyeShncmVlZHlfZW5zZW1ibGUpDQoNCmBgYA0KDQpL4bq/dCBxdeG6oyAqKlRoZSByZXN1bHRpbmcgQWNjdXJhY3kgaXM6IDAuOTAxNioqIG5naMSpYSBsw6AgRW5zZW1ibGUgTW9kZWwgY+G7p2EgY2jDum5nIHRhIGPDsyBBY2N1cmFjeSBsw6AgOTAuMTYlIChjaMO6IMO9IMSRw6J5IGzDoCBjb24gc+G7kSB0cnVuZyBiw6xuaCBraGkgdGjhu60gbmdoaeG7h20gdHLDqm4gMTAgbeG6q3UpLiANCg0KQ2jDum5nIHRhIG7Dqm4ga2jhuqNvIHPDoXQga8SpIGjGoW4ga2jhuqMgbsSDbmcgcGjDom4gbG/huqFpIGPhu6dhIG3DtCBow6xuaCBFbnNlbWJsZSBuw6B5IHbDoCBzbyBzw6FuaCB24bubaSBjw6FjIG3DtCBow6xuaCB0aMOgbmggcGjhuqduICJj4bqldSB04bqhbyIgbsOqbiBuw7M6IA0KDQpgYGB7ciwgZmlnLmZ1bGx3aWR0aCA9IFRSVUUsIGZpZy5oZWlnaHQ9NywgZmlnLndpZHRoPTEyfQ0KIyBBZGQgRW5zZW1ibGUgTW9kZWw6IA0KdG90YWxfZGZfZW4gPC0gYmluZF9yb3dzKHRvdGFsX2RmLCBncmVlZHlfZW5zZW1ibGUkZW5zX21vZGVsJHJlc2FtcGxlICU+JSBtdXRhdGUoTW9kZWwgPSAiZW5zZW1ibGUiKSkNCg0KdG90YWxfZGZfZW4gJT4lIA0KICBncm91cF9ieShNb2RlbCkgJT4lIA0KICBzdW1tYXJpc2UoYXZnX2FjYyA9IG1lYW4oQWNjdXJhY3kpKSAlPiUgDQogIHVuZ3JvdXAoKSAlPiUgDQogIGFycmFuZ2UoLWF2Z19hY2MpICU+JSANCiAgbXV0YXRlX2lmKGlzLm51bWVyaWMsIGZ1bmN0aW9uKHgpIHtyb3VuZCh4LCAzKX0pICU+JSANCiAga25pdHI6OmthYmxlKGNhcHRpb24gPSAiVGFibGUgMzogTW9kZWwgUGVyZm9ybWFuY2UgaW4gZGVjcmVhc2luZyBvcmRlciBvZiBBY2N1cmFjeSIsIA0KICAgICAgICAgICAgICAgY29sLm5hbWVzID0gYygiTW9kZWwiLCAiQXZlcmFnZSBBY2N1cmFjeSIpKQ0KDQp0b3RhbF9kZl9lbiAlPiUgDQogIGdyb3VwX2J5KE1vZGVsKSAlPiUgDQogIHN1bW1hcmlzZShhdmdfc2VuID0gbWVhbihTZW5zaXRpdml0eSkpICU+JSANCiAgdW5ncm91cCgpICU+JSANCiAgYXJyYW5nZSgtYXZnX3NlbikgJT4lIA0KICBtdXRhdGVfaWYoaXMubnVtZXJpYywgZnVuY3Rpb24oeCkge3JvdW5kKHgsIDMpfSkgJT4lIA0KICBrbml0cjo6a2FibGUoY2FwdGlvbiA9ICJUYWJsZSA0OiBNb2RlbCBQZXJmb3JtYW5jZSBpbiBkZWNyZWFzaW5nIG9yZGVyIG9mIFNlbnNpdGl2aXR5IiwgDQogICAgICAgICAgICAgICBjb2wubmFtZXMgPSBjKCJNb2RlbCIsICJBdmVyYWdlIFNlbnNpdGl2aXR5IikpDQoNCg0KYGBgDQoNCk3hurdjIGTDuSBFbnNlbWJsZSBNb2RlbCBjw7MgQWNjdXJhY3kgbeG7m2kgY2jhu4kgeOG6v3AgdGjhu6kgMiBzYXUgYWRhYm9vc3QgKGLhuqNuZyAzKSBuaMawbmcgxJHhurdjIMSRaeG7g20gc2F1IG3hu5tpIGzDoCBxdWFuIHRy4buNbmcgKHThu6sgYuG6o25nIDQpOiAqKmto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSDEkcO6bmcgY8OhYyBo4buTIHPGoSBCYWQgdMSDbmcgMzglIHThu6sgNjcuMzAlIGzDqm4gOTIuOSUqKi4gIA0KDQpDaMO6bmcgdGEgY8WpbmcgY8OzIHRo4buDIHPhu60gZOG7pW5nIGPDtG5nIGPhu6UgaMOsbmgg4bqjbmggxJHhu4MgxJHDoW5oIGdpw6Ega2jhuqMgbsSDbmcgcGjDom4gbG/huqFpIGPhu6dhIGPDoWMgbcO0IGjDrG5oOiANCg0KYGBge3IsIGZpZy5mdWxsd2lkdGggPSBUUlVFLCBmaWcuaGVpZ2h0PTEyLCBmaWcud2lkdGg9MTJ9DQp0aGVtZV9zZXQodGhlbWVfbWluaW1hbCgpKQ0KdG90YWxfZGZfZW4gJT4lIA0KICBzZWxlY3QoLWxvZ0xvc3MsIC1wckFVQywgLVJlc2FtcGxlKSAlPiUgDQogIGdhdGhlcihhLCBiLCAtTW9kZWwpICU+JSANCiAgZ2dwbG90KGFlcyhNb2RlbCwgYiwgZmlsbCA9IE1vZGVsLCBjb2xvciA9IE1vZGVsKSkgKyANCiAgZ2VvbV9ib3hwbG90KHNob3cubGVnZW5kID0gRkFMU0UsIGFscGhhID0gMC4zKSArIA0KICBmYWNldF93cmFwKH4gYSwgc2NhbGVzID0gImZyZWUiKSArIA0KICBjb29yZF9mbGlwKCkgKyANCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudCkgKyANCiAgdGhlbWUocGxvdC5tYXJnaW4gPSB1bml0KGMoMSwgMSwgMSwgMSksICJjbSIpKSArIA0KICBsYWJzKHggPSBOVUxMLCB5ID0gTlVMTCwgDQogICAgICAgdGl0bGUgPSAiQSBTaG9ydCBDb21wYXJpc2lvbjogRW5zZW1ibGUgQXBwcm9hY2ggdnMgU29tZSBNYWNoaW5lIExlYXJuaW5nIE1vZGVscyIsIA0KICAgICAgIHN1YnRpdGxlID0gIlZhbGRhdGlvbiBNZXRob2QgVXNlZDogQ3Jvc3MtdmFsaWRhdGlvbiwgMTAgc2FtcGxlcyIpDQpgYGANCg0KS2jhuqMgbsSDbmcgcGjDom4gbG/huqFpIGNow61uaCB4w6FjIGjhu5Mgc8ahIEJhZCBjw7MgdGjhu4MgxJHGsOG7o2Mga2nhu4NtIHRyYSBuZ2F5IHRyw6puIHRlc3QgZGF0YS4gRMaw4bubaSDEkcOieSBsw6Agc28gc8OhbmggRW5zZW1ibGUgdsOgIG3DtCBow6xuaCAidOG7kXQgbmjhuqV0IiBhZGFib29zdDogDQoNCmBgYHtyfQ0KbW9kZWxzX2NvbSA8LSBsaXN0KG1vZGVsX2xpc3QxJGFkYWJvb3N0LCBncmVlZHlfZW5zZW1ibGUpDQpsYXBwbHkobW9kZWxzX2NvbSwgZnVuY3Rpb24oeCkge2NvbmZ1c2lvbk1hdHJpeChwcmVkaWN0KHgsIGRmX3Rlc3RfbWwpLCBkZl90ZXN0X21sJEJBRCwgcG9zaXRpdmUgPSAiQmFkIil9KQ0KDQpgYGANCg0KRW5zZW1ibGUgTW9kZWwgcGjDom4gbG/huqFpIGNow61uaCB4w6FjIDI5OSBo4buTIHPGoSB44bqldSAodHJvbmcgdOG7lW5nIHPhu5EgdOG6pXQgY+G6oyAzOTYgaOG7kyBzxqEgeOG6pXUpLiBUcm9uZyBraGkgYWRhYm9vc3QgdGjDrCBjb24gc+G7kSBuw6B5IGNo4buJIGzDoCAyNDQuIE5ndXnDqm4gbmjDom4gY8OzIHRo4buDIMSR4bq/biB04burIHPhu7Ega2jDoWMgYmnhu4d0IGPhu6dhIFJPQyAvIEFVQy4gQ2jDum5nIHRhIGPFqW5nIGPDsyB0aOG7gyBraOG6o28gc8OhdCBST0MgLyBBVUMgY2hvIGhhaSBtw7QgaMOsbmggbsOgeTogDQoNCmBgYHtyLCBmaWcuZnVsbHdpZHRoID0gVFJVRSwgZmlnLmhlaWdodD0xMiwgZmlnLndpZHRoPTEyfQ0KIyAgR8OzaSBjaG8gdMOtbmggdG/DoW4gQVVDOiANCmxpYnJhcnkocFJPQykgDQoNCiMgVmnhur90IGjDoG0gdMOtbmggQVVDOiANCnRlc3RfYXVjX2Zvcl9lbnNlbWJsZSA8LSBmdW5jdGlvbihtb2RlbCkgew0KICByb2MoZGZfdGVzdF9tbCRCQUQsIHByZWRpY3QobW9kZWwsIGRmX3Rlc3RfbWwsIHR5cGUgPSAicHJvYiIpKQ0KfQ0KDQoNCnRlc3RfYXVjX2Zvcl9jYXJldF9vYmogPC0gZnVuY3Rpb24obW9kZWwpIHsNCiAgcm9jKGRmX3Rlc3RfbWwkQkFELCBwcmVkaWN0KG1vZGVsLCBkZl90ZXN0X21sLCB0eXBlID0gInByb2IiKSAlPiUgcHVsbChCYWQpKQ0KfQ0KDQoNCiMgU+G7rSBk4bulbmcgaMOgbSBuw6B5OiANCm15X2F1YzEgPC0gdGVzdF9hdWNfZm9yX2Vuc2VtYmxlKGdyZWVkeV9lbnNlbWJsZSkNCm15X2F1YzEkYXVjDQoNCm15X2F1YzIgPC0gdGVzdF9hdWNfZm9yX2NhcmV0X29iaihtb2RlbF9saXN0MSRhZGFib29zdCkNCm15X2F1YzIkYXVjDQoNCiMgREYgY2hvIHNvIHPDoW5oIFJPQyAvIEFVQzogDQpkZl9hdWMgPC0gYmluZF9yb3dzKGRhdGFfZnJhbWUoVFBSID0gbXlfYXVjMSRzZW5zaXRpdml0aWVzLCBGUFIgPSAxIC0gbXlfYXVjMSRzcGVjaWZpY2l0aWVzLCBNb2RlbCA9ICJFbnNlbWJsZSIpLCANCiAgICAgICAgICAgICAgICAgICAgZGF0YV9mcmFtZShUUFIgPSBteV9hdWMyJHNlbnNpdGl2aXRpZXMsIEZQUiA9IDEgLSBteV9hdWMyJHNwZWNpZmljaXRpZXMsIE1vZGVsID0gIkFkYWJvb3N0IikpDQoNCmRmX2F1YyAlPiUgDQogIGdncGxvdChhZXMoRlBSLCBUUFIsIGNvbG9yID0gTW9kZWwpKSArDQogIGdlb21fbGluZShzaXplID0gMSkgKw0KICB0aGVtZV9idygpICsNCiAgY29vcmRfZXF1YWwoKSArDQogIGdlb21fYWJsaW5lKGludGVyY2VwdCA9IDAsIHNsb3BlID0gMSwgY29sb3IgPSAiZ3JheTM3Iiwgc2l6ZSA9IDEsIGxpbmV0eXBlID0gImRhc2hlZCIpICsgDQogIGxhYnMoeCA9ICJGUFIgKDEgLSBTcGVjaWZpY2l0eSkiLCANCiAgICAgICB5ID0gIlRQUiAoU2Vuc2l0aXZpdHkpIiwgDQogICAgICAgdGl0bGUgPSAiUk9DIEN1cnZlIGFuZCBBVUM6IEVuc2VtYmxlIE1ldGhvZCB2cyBBZGFib29zdCIpDQoNCmBgYA0KDQojICBBIE1vcmUgU29waGlzdGljYXRlZCBFbnNlbWJsZSBNb2RlbA0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIHjDonkgZOG7sW5nIG3hu5l0IEVuc2VtYmxlIE1vZGVsIHBo4bupYyB04bqhcCBoxqFuIHbhu5tpIGjDoG0gKipjYXJldFN0YWNrKCkqKi4gxJBp4buDbSBj4bqnbiBjaMO6IMO9IGzDoCBsw7pjIG7DoHkgcGjhuqNpIHRoaeG6v3QgbOG6rXAgbeG7mXQgb2JqZWN0ICpjb250cm9sX25ldyogaG/DoG4gdG/DoG4gbeG7m2k6IA0KYGBge3J9DQoNCiMgSHXhuqVuIGx1eeG7h24gRW5zZW1ibGUgTW9kZWw6IA0Kc2V0LnNlZWQoMSkNCmNvbnRyb2xfbmV3IDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIG51bWJlciA9IG51bWJlciAsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlcGVhdHMgPSByZXBlYXRzLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdW1tYXJ5RnVuY3Rpb24gPSBtdWx0aUNsYXNzU3VtbWFyeSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxsb3dQYXJhbGxlbCA9IFRSVUUpDQoNCmdibV9lbnNlbWJsZSA8LSBjYXJldFN0YWNrKG1vZGVsX2xpc3QxLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJnYm0iLCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICANCiAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldHJpYyA9ICJBY2N1cmFjeSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlID0gRkFMU0UsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gY29udHJvbF9uZXcpDQoNCg0KIyBTbyBzw6FuaCB24bubaSBuaOG7r25nIG1vZGVscyDEkcOjIGPDszogDQoNCnRvdGFsX2RmX2VuMiA8LSBiaW5kX3Jvd3ModG90YWxfZGZfZW4sIGdibV9lbnNlbWJsZSRlbnNfbW9kZWwkcmVzYW1wbGUgJT4lIG11dGF0ZShNb2RlbCA9ICJlbnNlbWJsZTIiKSkNCg0KdG90YWxfZGZfZW4yICU+JSANCiAgZ3JvdXBfYnkoTW9kZWwpICU+JSANCiAgc3VtbWFyaXNlKGF2Z19hY2MgPSBtZWFuKEFjY3VyYWN5KSkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBhcnJhbmdlKC1hdmdfYWNjKSAlPiUgDQogIG11dGF0ZV9pZihpcy5udW1lcmljLCBmdW5jdGlvbih4KSB7cm91bmQoeCwgMyl9KSAlPiUgDQogIGtuaXRyOjprYWJsZShjYXB0aW9uID0gIlRhYmxlIDU6IE1vZGVsIFBlcmZvcm1hbmNlIGluIGRlY3JlYXNpbmcgb3JkZXIgb2YgQWNjdXJhY3kiLCANCiAgICAgICAgICAgICAgIGNvbC5uYW1lcyA9IGMoIk1vZGVsIiwgIkF2ZXJhZ2UgQWNjdXJhY3kiKSkNCg0KdG90YWxfZGZfZW4yICU+JSANCiAgZ3JvdXBfYnkoTW9kZWwpICU+JSANCiAgc3VtbWFyaXNlKGF2Z19zZW4gPSBtZWFuKFNlbnNpdGl2aXR5KSkgJT4lIA0KICB1bmdyb3VwKCkgJT4lIA0KICBhcnJhbmdlKC1hdmdfc2VuKSAlPiUgDQogIG11dGF0ZV9pZihpcy5udW1lcmljLCBmdW5jdGlvbih4KSB7cm91bmQoeCwgMyl9KSAlPiUgDQogIGtuaXRyOjprYWJsZShjYXB0aW9uID0gIlRhYmxlIDY6IE1vZGVsIFBlcmZvcm1hbmNlIGluIGRlY3JlYXNpbmcgb3JkZXIgb2YgU2Vuc2l0aXZpdHkiLCANCiAgICAgICAgICAgICAgIGNvbC5uYW1lcyA9IGMoIk1vZGVsIiwgIkF2ZXJhZ2UgU2Vuc2l0aXZpdHkiKSkNCg0KYGBgDQoNCkPDsyB0aOG7gyB0aOG6pXkgR0JNIEVuc2VtYmxlIE1vZGVsIGPDsyBBY2N1cmFjeSBjYW8gaMahbiDEkcOhbmcga+G7gyBzbyB24bubaSBFbnNlbWJsZSDEkeG6p3UgdGnDqm4gKExpbmVhciBFbnNlbWJsZSkuIE5oxrBuZyBy4bqldCBjw7MgdGjhu4MgR0JNIEVuc2VtYmxlIE1vZGVsIGNoxrBhIGjhurNuIGzDoCBtw7QgaMOsbmggcGjDom4gbG/huqFpIHThu5F0IG5o4bqldCB2w6wgU2Vuc2l0aXZpdHkgLSB04bupYyBraOG6oyBuxINuZyBwaMOibiBsb+G6oWkgY2jDrW5oIHjDoWMgaOG7kyBzxqEgQmFkIHRo4bqlcCBoxqFuIGPFqW5nIMSRw6FuZyBr4buDLiBDaHVuZyB0YSBuw6puIGtp4buDbSB0cmEgdHLDqm4gdGVzdCBkYXRhIMSR4buDIGPDsyDEkcOhbmggZ2nDoSB0b8OgbiBkaeG7h24gaMahbjogDQoNCmBgYHtyfQ0KY29uZnVzaW9uTWF0cml4KHByZWRpY3QoZ2JtX2Vuc2VtYmxlLCBkZl90ZXN0X21sKSwgZGZfdGVzdF9tbCRCQUQsIHBvc2l0aXZlID0gIkJhZCIpDQpgYGANCg0KR0JNIEVuc2VtYmxlIHBow6JuIGxv4bqhaSDEkcO6bmcgMzAzIGPDoWMgaOG7kyBzxqEgQmFkIHRyb25nIHThu5VuZyBz4buRIDM1NiB0csaw4budbmcgaOG7o3AuIFThu6ljIG3hu6ljIMSR4buZIGNow61uaCB4w6FjIGtoaSBwaMOibiBsb+G6oWkgbmjDs20gaOG7kyBzxqEgeOG6pXUgbMOgIDg1LjExJS4gxJDDonkgbMOgIG5o4buvbmcgZOG6pXUgaGnhu4d1IGNobyB0aOG6pXkgR0JNIEVuc2VtYmxlIGzDoCBtw7QgaMOsbmggdOG7kXQgbmjhuqV0LiBUdXkgbmhpw6puIMSR4buDIGPDsyBr4bq/dCBsdeG6rW4gY2jhuq9jIGNo4bqvbiBoxqFuIGNow7puZyB0YSBj4bqnbiDEkcOhbmggZ2nDoSB4YSBoxqFuIG5nYXkgc2F1IMSRw6J5LiANCg0KIyBBIFNob3J0IENvbXBhcmlzaW9uIGJldHdlZW4gR0JNIGFuZCBMaW5lYXIgRW5zZW1ibGUNCg0KxJDhu4Mgc28gc8OhbmggdG/DoG4gZGnhu4duIGjGoW4gR0JNIHbDoCBMaW5lYXIgRW5zZW1ibGUgY2jDum5nIHRhIHZp4bq/dCBoYWkgaMOgbSB0xrDGoW5nIHThu7EgbmhhdSBuaMawIHbhu4EgY2jhu6ljIG7Eg25nLiBD4bulIHRo4buDLCBow6BtIHPhur0gxJHDoW5oIGdpw6EgY2jhu6tuZyAxMiB0acOqdSBjaMOtIHBow6JuIGxv4bqhaSBj4bunYSBtw7QgaMOsbmggZOG7sWEgdHLDqm4gMTUgbOG6p24gY2jhu41uIG3huqt1LCBt4buXaSBt4buZdCBs4bqnbiBs4bqleSA1MCUgcXVhbiBzw6F0IHThu6sgdGVzdCBkYXRhOiANCg0KDQpgYGB7cn0NCiMgRnVuY3Rpb24gMTogDQoNCmV2YWxfZnVuX2xpbmVhciA8LSBmdW5jdGlvbih0aHJlKSB7DQogIGxhcHBseSgxOjE1LCBmdW5jdGlvbih4KSB7DQogICAgDQogICAgc2V0LnNlZWQoeCkNCiAgICBpZCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSBkZl90ZXN0X21sJEJBRCwgcCA9IDAuNSwgbGlzdCA9IEZBTFNFKQ0KICAgIHRlc3RfZGYgPC0gZGZfdGVzdF9tbFtpZCwgXQ0KICANCiAgICBkdV9iYW9fcHJvYiA8LSAxIC0gcHJlZGljdChncmVlZHlfZW5zZW1ibGUsIHRlc3RfZGYsIHR5cGUgPSAicHJvYiIpDQogICAgDQogICAgZHVfYmFvIDwtIGNhc2Vfd2hlbihkdV9iYW9fcHJvYiA+PSB0aHJlIH4gIkJhZCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgZHVfYmFvX3Byb2IgPCB0aHJlIH4gIkdvb2QiKSAlPiUgYXMuZmFjdG9yKCkNCiAgICANCiAgICBjbSA8LSBjb25mdXNpb25NYXRyaXgoZHVfYmFvLCB0ZXN0X2RmJEJBRCwgcG9zaXRpdmUgPSAiQmFkIikNCiAgICANCiAgICBiZ19nZyA8LSBjbSR0YWJsZSAlPiUgDQogICAgICBhcy52ZWN0b3IoKSAlPiUgDQogICAgICBtYXRyaXgobmNvbCA9IDQpICU+JSANCiAgICAgIGFzLmRhdGEuZnJhbWUoKSAlPiUgDQogICAgICByZW5hbWUoVFAgPSBWMSwgRk4gPSBWMiwgRlAgPSBWMywgVE4gPSBWNCkNCiAgDQogICAga3EgPC0gYyhjbSRvdmVyYWxsLCBjbSRieUNsYXNzKSANCiAgICB0ZW4gPC0ga3EgJT4lIGFzLmRhdGEuZnJhbWUoKSAlPiUgcm93Lm5hbWVzKCkNCiAgICANCiAgICBrcSAlPiUgDQogICAgICBhcy52ZWN0b3IoKSAlPiUgDQogICAgICBtYXRyaXgobmNvbCA9IDE4KSAlPiUgDQogICAgICBhcy5kYXRhLmZyYW1lKCkgLT4gYWxsX2RmDQogICAgDQogICAgbmFtZXMoYWxsX2RmKSA8LSB0ZW4NCiAgICBhbGxfZGYgPC0gYmluZF9jb2xzKGFsbF9kZiwgYmdfZ2cpDQogICAgcmV0dXJuKGFsbF9kZikNCiAgfSkNCn0NCg0KDQoNCiMgRnVuY3Rpb24gMjogDQoNCmV2YWxfZnVuX2dibSA8LSBmdW5jdGlvbih0aHJlKSB7DQogIGxhcHBseSgxOjE1LCBmdW5jdGlvbih4KSB7DQogICAgDQogICAgc2V0LnNlZWQoeCkNCiAgICBpZCA8LSBjcmVhdGVEYXRhUGFydGl0aW9uKHkgPSBkZl90ZXN0X21sJEJBRCwgcCA9IDAuNSwgbGlzdCA9IEZBTFNFKQ0KICAgIHRlc3RfZGYgPC0gZGZfdGVzdF9tbFtpZCwgXQ0KICANCiAgICBkdV9iYW9fcHJvYiA8LSAxIC0gcHJlZGljdChnYm1fZW5zZW1ibGUsIHRlc3RfZGYsIHR5cGUgPSAicHJvYiIpDQogICAgDQogICAgZHVfYmFvIDwtIGNhc2Vfd2hlbihkdV9iYW9fcHJvYiA+PSB0aHJlIH4gIkJhZCIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgZHVfYmFvX3Byb2IgPCB0aHJlIH4gIkdvb2QiKSAlPiUgYXMuZmFjdG9yKCkNCiAgICANCiAgICBjbSA8LSBjb25mdXNpb25NYXRyaXgoZHVfYmFvLCB0ZXN0X2RmJEJBRCwgcG9zaXRpdmUgPSAiQmFkIikNCiAgICANCiAgICBiZ19nZyA8LSBjbSR0YWJsZSAlPiUgDQogICAgICBhcy52ZWN0b3IoKSAlPiUgDQogICAgICBtYXRyaXgobmNvbCA9IDQpICU+JSANCiAgICAgIGFzLmRhdGEuZnJhbWUoKSAlPiUgDQogICAgICByZW5hbWUoVFAgPSBWMSwgRk4gPSBWMiwgRlAgPSBWMywgVE4gPSBWNCkNCiAgDQogICAga3EgPC0gYyhjbSRvdmVyYWxsLCBjbSRieUNsYXNzKSANCiAgICB0ZW4gPC0ga3EgJT4lIGFzLmRhdGEuZnJhbWUoKSAlPiUgcm93Lm5hbWVzKCkNCiAgICANCiAgICBrcSAlPiUgDQogICAgICBhcy52ZWN0b3IoKSAlPiUgDQogICAgICBtYXRyaXgobmNvbCA9IDE4KSAlPiUgDQogICAgICBhcy5kYXRhLmZyYW1lKCkgLT4gYWxsX2RmDQogICAgDQogICAgbmFtZXMoYWxsX2RmKSA8LSB0ZW4NCiAgICBhbGxfZGYgPC0gYmluZF9jb2xzKGFsbF9kZiwgYmdfZ2cpDQogICAgcmV0dXJuKGFsbF9kZikNCiAgfSkNCn0NCg0KIyDEkMOhbmggZ2nDoSBz4buxIGJp4bq/biDEkeG7lWkgdGhlbyBt4buZdCBsb+G6oXQgbmfGsOG7oW5nOiANCnN5c3RlbS50aW1lKHNvX3NhbmhfbGlzdDEgPC0gbGFwcGx5KHNlcSgwLjA1LCAwLjgsIGJ5ID0gMC4wNSksIGV2YWxfZnVuX2xpbmVhcikpDQpzeXN0ZW0udGltZShzb19zYW5oX2xpc3QyIDwtIGxhcHBseShzZXEoMC4wNSwgMC44LCBieSA9IDAuMDUpLCBldmFsX2Z1bl9nYm0pKQ0KDQpzb19zYW5oX2RmMSA8LSBkby5jYWxsKCJiaW5kX3Jvd3MiLCBzb19zYW5oX2xpc3QxKSANCnNvX3NhbmhfZGYxICU8PiUgDQogIG11dGF0ZShUaHJlc2hvbGQgPSBsYXBwbHkoc2VxKDAuMDUsIDAuOCwgYnkgPSAwLjA1KSwgZnVuY3Rpb24oeCkge3JlcCh4LCAxNSl9KSAlPiUgdW5saXN0KCkpDQoNCnNvX3NhbmhfZGYyIDwtIGRvLmNhbGwoImJpbmRfcm93cyIsIHNvX3NhbmhfbGlzdDIpIA0Kc29fc2FuaF9kZjIgJTw+JSANCiAgbXV0YXRlKFRocmVzaG9sZCA9IGxhcHBseShzZXEoMC4wNSwgMC44LCBieSA9IDAuMDUpLCBmdW5jdGlvbih4KSB7cmVwKHgsIDE1KX0pICU+JSB1bmxpc3QoKSkNCg0KIyBU4buVbmcgaOG7o3AgY8OhYyBr4bq/dCBxdeG6ozogDQpkZl9jb21fZW5zZW1ibGUgPC0gYmluZF9yb3dzKHNvX3NhbmhfZGYxICU+JSBtdXRhdGUoTW9kZWwgPSAiTGluZWFyRW5zZW1ibGUiKSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNvX3NhbmhfZGYyICU+JSBtdXRhdGUoTW9kZWwgPSAiR0JNX0Vuc2VtYmxlIikpDQoNCg0KZGZfY29tX2Vuc2VtYmxlICU+JSANCiAgc2VsZWN0KEFjY3VyYWN5LCBTZW5zaXRpdml0eSwgU3BlY2lmaWNpdHksIFJlY2FsbCwgTW9kZWwsIFRocmVzaG9sZCkgJT4lIA0KICBncm91cF9ieShUaHJlc2hvbGQsIE1vZGVsKSAlPiUgDQogIHN1bW1hcmlzZV9lYWNoKGZ1bnMobWVhbiksIEFjY3VyYWN5LCBTZW5zaXRpdml0eSwgU3BlY2lmaWNpdHksIFJlY2FsbCkgJT4lIA0KICBnYXRoZXIoYSwgYiwgLVRocmVzaG9sZCwgLU1vZGVsKSAlPiUgDQogIGdncGxvdChhZXMoVGhyZXNob2xkLCBiLCBjb2xvciA9IE1vZGVsKSkgKyANCiAgZ2VvbV9saW5lKCkgKyANCiAgZ2VvbV9wb2ludCgpICsgDQogIGZhY2V0X3dyYXAofiBhLCBzY2FsZXMgPSAiZnJlZSIpICsgDQogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsgDQogIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXMgPSBjKCIjZTQxYTFjIiwgIiMzNzdlYjgiKSwgbmFtZSA9ICIiKSArIA0KICB0aGVtZShwYW5lbC5ncmlkLm1pbm9yLnkgID0gZWxlbWVudF9ibGFuaygpKSArIA0KICBsYWJzKHkgPSAiQWNjdXJhY3kgUmF0ZSIsIA0KICAgICAgIHRpdGxlID0gIkEgU2hvcnQgQ29tcGFyaXNpb246IEdCTSBhbmQgTGluZWFyIEVuc2VtYmxlIEFwcHJvYWNoIikNCg0KDQpgYGANCg0KS+G6v3QgbHXhuq1uIHF1YW4gdHLhu41uZyBsw6A6IA0KDQoxLiBU4bqhaSBt4buNaSBuZ8aw4buhbmcgdGjDrCBiYSB0cm9uZyBi4buRbiB0acOqdSBjaMOtIEdCTSBFbnNlbWJsZSDEkeG7gXUgY2FvIGjGoW4uIA0KDQoyLiBBY2N1cmFjeSBsw6AgbeG7mXQgxJHGsOG7nW5nIGNvbmcgYuG6rWMgaGFpIGjDrG5oIGNo4buvIFUgbmfGsOG7o2MuIE5ndXnDqm4gbmjDom4gbMOgIGRvIHPhu7EgxJHDoW5oIMSR4buVaSBnaeG7r2EgaGFpIHRo4bupIHNhdToga2jhuqMgbsSDbmcgcGjDom4gbG/huqFpIGjhu5Mgc8ahIHThu5F0IGPDoG5nIGNhbyB0aMOsIGto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSBo4buTIHPGoSB04buRdCBz4bq9IGdp4bqjbS4gDQoNCjMuIELhuq90IMSR4bqndSB04burIG5nxrDhu6FuZyAwLjM1IHRow6wgY8OhYyB0acOqdSBjaMOtIMSRw6FuaCBnacOhIGto4bqjIG7Eg25nIHBow6JuIGxv4bqhaSBj4bunYSBHQk0gRW5zZW1ibGUgbHXDtG4gY2FvIGjGoW4gTGluZWFyIEVuc2VtYmxlLiANCg0KNC4gVOG7qyAoMSksICgyKSB2w6AgKDMpIHRow6wgcsO1IHLDoG5nIEdCTSBFbnNlbWJsZSBuw6puIMSRxrDhu6NjIGPDoWMgdOG7lSBjaOG7qWMgdMOgaSBjaMOtbmggbOG7sWEgY2jhu41uIG5oxrAgbMOgIG3DtCBow6xuaCBwaMOibiBsb+G6oWkgdsOgIHjhur9wIGjhuqFuZyBDcmVkaXQgQXBwbGljYXRpb24uIA0KDQo=