devtools::load_all() # if using the rproject dowloaded from the slides
# source("utils-glm.R") # if using a standard setup
library(here)
library(tidyr) # for data manipulation
library(dplyr) # for data manipulation
library(ggplot2) # plotting
library(performance) # diagnostic
library(car) # general utilities
library(MuMIn) # model selection

Overview1

We are gonna work with the admission.csv dataset containing \(n = 400\) students for the admission to the UCLA University. A researcher is interested in how variables, such as gre (Graduate Record Exam scores), gpa (GPA), rank (prestige of the undergraduate institution) have an influence for the admission into graduate school. The response variable, admit/don’t admit, is a binary variable.

  1. Importing data and check
  2. Exploratory data analysis
  3. Model fitting with glm()
  4. Model diagnostic
  5. Interpreting parameters
  6. Model selection

1. Importing data

We need to set the working directory on the root of the course folder using set.wd(). Using R Projects is just necessary to open the .RProj file and the working directory will be automatically correctly selected.

# reading data
admission <- read.csv(here("data", "admission.csv"), header=TRUE)

# first rows
head(admission)
##   admit gre   gpa rank
## 1     0 456 5.571    3
## 2     1 792 5.637    3
## 3     1 960 6.000    1
## 4     1 768 5.109    4
## 5     0 624 4.823    4
## 6     1 912 4.900    2
# check dataset structure
str(admission)
## 'data.frame':    400 obs. of  4 variables:
##  $ admit: int  0 1 1 1 0 1 1 0 1 0 ...
##  $ gre  : int  456 792 960 768 624 912 672 480 648 840 ...
##  $ gpa  : num  5.57 5.64 6 5.11 4.82 ...
##  $ rank : int  3 3 1 4 4 2 1 2 3 2 ...
# summary statistics
summary(admission)
##      admit             gre             gpa             rank      
##  Min.   :0.0000   Min.   :264.0   Min.   :4.086   Min.   :1.000  
##  1st Qu.:0.0000   1st Qu.:624.0   1st Qu.:5.043   1st Qu.:2.000  
##  Median :0.0000   Median :696.0   Median :5.335   Median :2.000  
##  Mean   :0.3175   Mean   :705.5   Mean   :5.329   Mean   :2.485  
##  3rd Qu.:1.0000   3rd Qu.:792.0   3rd Qu.:5.637   3rd Qu.:3.000  
##  Max.   :1.0000   Max.   :960.0   Max.   :6.000   Max.   :4.000

It is very important that each variable is correctly interpreted by R:

  • admit is a binary variable stored as integer (0 and 1)
  • gre is a numerical variable stored as integer
  • gpa is a numerical variables stored as double precision number
  • rank is a numerical variables stored as integer

We could change the type of rank to factor because we are going to use it as a categorical (maybe ordinal) variable.

admission$rankc <- factor(admission$rank, levels = 1:4, labels = 1:4)

2. Exploratory data analysis

We can plot the univariate distribution of each variable:

# gre and gpa
admission |> 
    select(gre, gpa) |> 
    pivot_longer(1:2) |> 
    ggplot(aes(x = value)) +
    geom_histogram(col = "black",
                   fill = "lightblue") +
    facet_wrap(~name, scales = "free")

admission |> 
    ggplot(aes(x = rank)) +
    geom_bar()

admission |> 
    ggplot(aes(x = admit)) +
    geom_bar()

Then we can cut the gpa and gre variabiles into categories and plot the admissions for each bin (i.e., a contingency table):

admission$gpa_c <- cut(admission$gpa, seq(4, 6, 0.2), labels = FALSE)
admission$gre_c <- cut(admission$gre, seq(260, 960, 50), labels=FALSE)
# admission ~ gpa
admission |> 
    ggplot(aes(x = gpa_c, fill = factor(admit))) +
    geom_bar(position = position_dodge()) +
    labs(fill = "Admission") +
    theme(legend.position = "bottom")

# admission ~ gre
admission |> 
    ggplot(aes(x = gre_c, fill = factor(admit))) +
    geom_bar(position = position_dodge()) +
    labs(fill = "Admission") +
    theme(legend.position = "bottom")

Given that the number of admitted is lower than the number of non admitted, we can have a look at the proportion of admission for each bin:

admission |> 
    group_by(gpa_c) |> 
    summarise(admit = mean(admit),
              non_admit = 1 - admit) |> 
    pivot_longer(2:3) |> 
    ggplot(aes(x = factor(gpa_c), y = value, fill = name)) +
    geom_col() +
    labs(fill = "Admission",
         y = "%",
         x = "gpa") +
    theme(legend.position = "bottom")

admission |> 
    group_by(gre_c) |> 
    summarise(admit = mean(admit),
              non_admit = 1 - admit) |> 
    pivot_longer(2:3) |> 
    ggplot(aes(x = factor(gre_c), y = value, fill = name)) +
    geom_col() +
    labs(fill = "Admission",
         y = "%",
         x = "gpa") +
    theme(legend.position = "bottom")

Finally we can have a look at the admissions as a function of the rank of the undergrad institution:

# margin = 2 means that each colum will sum to 1
prop.table(table(admission$admit, admission$rank), margin = 2)
##    
##             1         2         3         4
##   0 0.4590164 0.6423841 0.7685950 0.8208955
##   1 0.5409836 0.3576159 0.2314050 0.1791045

Clearly as the rank of the institute decrease (from 1 to 4) also the proportions of admissions decrease.

3. Model fitting with glm()

Now we ca fit the model using glm(). Let’s start by fitting a null model with no predictors. We choose a binomial glm with a logit link function.

fit0 <- glm(admit ~ 1, data = admission, family = binomial(link = "logit"))
summary(fit0)
## 
## Call:
## glm(formula = admit ~ 1, family = binomial(link = "logit"), data = admission)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -0.8741  -0.8741  -0.8741   1.5148   1.5148  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  -0.7653     0.1074  -7.125 1.04e-12 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 499.98  on 399  degrees of freedom
## Residual deviance: 499.98  on 399  degrees of freedom
## AIC: 501.98
## 
## Number of Fisher Scoring iterations: 4

Then we can fit the full model by putting all predictors:

fit1 <- glm(admit ~ gre + gpa + rankc, family = binomial(link = "logit"), data = admission)
summary(fit1)
## 
## Call:
## glm(formula = admit ~ gre + gpa + rankc, family = binomial(link = "logit"), 
##     data = admission)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.6302  -0.8665  -0.6374   1.1491   2.0833  
## 
## Coefficients:
##               Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -5.1607502  1.5547311  -3.319 0.000902 ***
## gre          0.0019360  0.0009107   2.126 0.033509 *  
## gpa          0.7245343  0.3017552   2.401 0.016347 *  
## rankc2      -0.6755746  0.3165961  -2.134 0.032854 *  
## rankc3      -1.3412412  0.3453868  -3.883 0.000103 ***
## rankc4      -1.5509436  0.4179394  -3.711 0.000207 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 499.98  on 399  degrees of freedom
## Residual deviance: 458.27  on 394  degrees of freedom
## AIC: 470.27
## 
## Number of Fisher Scoring iterations: 4

4. Model diagnostic

Firstly we can have a look to the residual ~ fitted plot:

plot_resid(fit1, type = "pearson")

Given that the admit is a binary variables and we are using a bernoulli model we can use the binned residuals to have a better idea:

binres <- data.frame(performance::binned_residuals(fit1, n_bins = 20))

binres |> 
    ggplot(aes(x = xbar, y = ybar)) +
    geom_point() +
    geom_line(aes(x = xbar, y = 2*se)) +
    geom_line(aes(x = xbar, y = -2*se)) +
    ylim(c(-0.5,0.5)) +
    xlab("Binned fitted(fit)") +
    ylab("Binned residuals(fit)")

Then we can check each predictors as a function of residuals:

residualPlots(fit1, tests = FALSE)

Then we can check for influential observations:

infl <- infl_measure(fit1)
head(infl)
##         dfb.1_     dfb.gre     dfb.gpa      dfb.rnk2     dfb.rnk3      dfb.rnk4
## 1  0.001928838 0.054851573 -0.02429314 -0.0005685401 -0.027482122  0.0011303580
## 2 -0.032365665 0.030589434  0.01842224  0.0011855080  0.093214421  0.0018889694
## 3 -0.043753262 0.047057023  0.03647553 -0.0692282501 -0.067292670 -0.0508865383
## 4  0.038875345 0.049085495 -0.05725416 -0.0021779468  0.005243727  0.1674589133
## 5 -0.021997746 0.004348061  0.01925034  0.0009501938 -0.001356376 -0.0370388854
## 6  0.069347733 0.137651326 -0.12263325  0.0568266645  0.011865765  0.0004088698
##         dffit     cov.r       cook.d        hat
## 1 -0.07289093 1.0267984 0.0005671995 0.01604128
## 2  0.15369752 0.9935677 0.0044945856 0.01089440
## 3  0.11248746 1.0312387 0.0014330276 0.02332145
## 4  0.22676575 0.9862142 0.0132208550 0.01672049
## 5 -0.05402182 1.0254753 0.0003020368 0.01316096
## 6  0.19463628 1.0104116 0.0062497431 0.02134213

Plotting using car

car::influenceIndexPlot(fit1, vars = c("Studentized", "hat", "Cook"))

Plotting also the dfbeta:

dfbeta_plot(fit1)

Check if there are observations with high standardized (studentized) residuals:

outlierTest(fit1) # Testing outliers
## No Studentized residuals with Bonferroni p < 0.05
## Largest |rstudent|:
##     rstudent unadjusted p-value Bonferroni p
## 198 2.110769           0.034792           NA

For potentially influential observations we could fir a model subtracting that specific observation and compare coefficients. This is similar to the dfbeta metric that suggest no influential observations on model parameters.

# Is 198 really influential?
fit1_no198 <- update(fit1, subset=-c(198))
compareCoefs(fit1, fit1_no198)
## Calls:
## 1: glm(formula = admit ~ gre + gpa + rankc, family = binomial(link = 
##   "logit"), data = admission)
## 2: glm(formula = admit ~ gre + gpa + rankc, family = binomial(link = 
##   "logit"), data = admission, subset = -c(198))
## 
##              Model 1  Model 2
## (Intercept)    -5.16    -5.25
## SE              1.55     1.56
##                              
## gre         0.001936 0.002097
## SE          0.000911 0.000919
##                              
## gpa            0.725    0.720
## SE             0.302    0.303
##                              
## rankc2        -0.676   -0.675
## SE             0.317    0.317
##                              
## rankc3        -1.341   -1.340
## SE             0.345    0.346
##                              
## rankc4        -1.551   -1.645
## SE             0.418    0.427
## 

5. Interpreting parameters

Firstly, we can extract model parameters, taking the exponential to interpret them as odds ratios:

broom::tidy(fit1, exponentiate = TRUE, conf.int = TRUE)
## # A tibble: 6 × 7
##   term        estimate std.error statistic  p.value conf.low conf.high
##   <chr>          <dbl>     <dbl>     <dbl>    <dbl>    <dbl>     <dbl>
## 1 (Intercept)  0.00574  1.55         -3.32 0.000902 0.000256     0.115
## 2 gre          1.00     0.000911      2.13 0.0335   1.00         1.00 
## 3 gpa          2.06     0.302         2.40 0.0163   1.15         3.76 
## 4 rankc2       0.509    0.317        -2.13 0.0329   0.272        0.945
## 5 rankc3       0.262    0.345        -3.88 0.000103 0.131        0.511
## 6 rankc4       0.212    0.418        -3.71 0.000207 0.0907       0.471

We can interpret these parameters as: for a unit increase in the x, the odds of being accepted in grad school increase by exp(beta). If we multiply the exp(beta)*100 we obtain the expected increase in percentage. Given that we have multiple parameters, when we intepret a specific parameter we are controlling for other parameters.

broom::tidy(fit1, exponentiate = TRUE, conf.int = TRUE) |>
    slice(-1) |> 
    mutate(estperc = estimate * 100)
## # A tibble: 5 × 8
##   term   estimate std.error statistic  p.value conf.low conf.high estperc
##   <chr>     <dbl>     <dbl>     <dbl>    <dbl>    <dbl>     <dbl>   <dbl>
## 1 gre       1.00   0.000911      2.13 0.0335     1.00       1.00    100. 
## 2 gpa       2.06   0.302         2.40 0.0163     1.15       3.76    206. 
## 3 rankc2    0.509  0.317        -2.13 0.0329     0.272      0.945    50.9
## 4 rankc3    0.262  0.345        -3.88 0.000103   0.131      0.511    26.2
## 5 rankc4    0.212  0.418        -3.71 0.000207   0.0907     0.471    21.2

To better interpret the parameters we need to make sure that the scale is meaningful. For example, the gre effect seems to be very small but statistically significant. The reason is that a unit increase in gre is very small. We could for example rescale the variable dividing for a constant term:

par(mfrow = c(1,2))
hist(admission$gre)
hist(admission$gre/100)

Let’s try fitting the model with the new variable:

admission$gre100 <- admission$gre/100
fit2 <- glm(admit ~ gre100 + gpa + rankc, family = binomial(link = "logit"), data = admission)

summary(fit2)
## 
## Call:
## glm(formula = admit ~ gre100 + gpa + rankc, family = binomial(link = "logit"), 
##     data = admission)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -1.6302  -0.8665  -0.6374   1.1491   2.0833  
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -5.16075    1.55473  -3.319 0.000902 ***
## gre100       0.19360    0.09107   2.126 0.033509 *  
## gpa          0.72453    0.30176   2.401 0.016347 *  
## rankc2      -0.67557    0.31660  -2.134 0.032854 *  
## rankc3      -1.34124    0.34539  -3.883 0.000103 ***
## rankc4      -1.55094    0.41794  -3.711 0.000207 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 499.98  on 399  degrees of freedom
## Residual deviance: 458.27  on 394  degrees of freedom
## AIC: 470.27
## 
## Number of Fisher Scoring iterations: 4
broom::tidy(fit2, exponentiate = TRUE, conf.int = TRUE) |>
    slice(-1) |> 
    mutate(estperc = estimate * 100)
## # A tibble: 5 × 8
##   term   estimate std.error statistic  p.value conf.low conf.high estperc
##   <chr>     <dbl>     <dbl>     <dbl>    <dbl>    <dbl>     <dbl>   <dbl>
## 1 gre100    1.21     0.0911      2.13 0.0335     1.02       1.45    121. 
## 2 gpa       2.06     0.302       2.40 0.0163     1.15       3.76    206. 
## 3 rankc2    0.509    0.317      -2.13 0.0329     0.272      0.945    50.9
## 4 rankc3    0.262    0.345      -3.88 0.000103   0.131      0.511    26.2
## 5 rankc4    0.212    0.418      -3.71 0.000207   0.0907     0.471    21.2

Now the gre effect is more meaningful. Notice how the overall model fitting is not changed togheter with other parameters. We are only rescaling variables.

Generally we can plot the effects for a better overview of the model:

plot(effects::allEffects(fit1))

To interpret the parameters in probability terms we could use the divide by 4 rule that express the maximum slope (i.e., the maximum probability increase):

coef(fit2)[-1]/4
##      gre100         gpa      rankc2      rankc3      rankc4 
##  0.04840012  0.18113356 -0.16889365 -0.33531031 -0.38773590

Similarly we can compute the marginal effects for each variable that represents the average slope:

margins::margins(fit2) |> summary()
##  factor     AME     SE       z      p   lower   upper
##     gpa  0.1409 0.0572  2.4608 0.0139  0.0287  0.2531
##  gre100  0.0376 0.0174  2.1660 0.0303  0.0036  0.0717
##  rankc2 -0.1565 0.0736 -2.1269 0.0334 -0.3007 -0.0123
##  rankc3 -0.2872 0.0733 -3.9192 0.0001 -0.4308 -0.1436
##  rankc4 -0.3210 0.0802 -4.0022 0.0001 -0.4781 -0.1638

Beyond the model coefficients, we could use a likelihood ratio test. Let’s start by comparing the null model with the current model. We hope that our variables combinations are doing a better job compared to a null model:

anova(fit0, fit1, test = "LRT")
## Analysis of Deviance Table
## 
## Model 1: admit ~ 1
## Model 2: admit ~ gre + gpa + rankc
##   Resid. Df Resid. Dev Df Deviance  Pr(>Chi)    
## 1       399     499.98                          
## 2       394     458.27  5   41.702 6.767e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

As expected from model summary and the deviance reduction, the variables are useful to predict the probability of admission. How useful? we could use some \(R^2\)-like measures:

performance::r2_tjur(fit1)
## Tjur's R2 
## 0.1023616

Despite useful, the model has a low \(R^2\). Furthermore the correct classification rate is higher than the chance level but relatively low:

1 - error_rate(fit1)
## [1] 0.71

7. Model selection

We could try a model comparison starting from the null model and finishing to the overall model:

fit2 <- update(fit2, na.action = na.fail) # required for mumin
dredge(fit2)
## Global model call: glm(formula = admit ~ gre100 + gpa + rankc, family = binomial(link = "logit"), 
##     data = admission, na.action = na.fail)
## ---
## Model selection table 
##   (Intrc)    gpa  gr100 rankc df   logLik  AICc delta weight
## 8 -5.1610 0.7245 0.1936     +  6 -229.137 470.5  0.00  0.699
## 6 -4.9940 0.9564            +  5 -231.438 473.0  2.54  0.196
## 7 -1.8330        0.2728     +  5 -232.088 474.3  3.84  0.102
## 5  0.1643                   +  4 -237.483 483.1 12.58  0.001
## 4 -6.0450 0.6806 0.2279        3 -240.054 486.2 15.68  0.000
## 3 -2.9250        0.3016        2 -242.861 489.8 19.26  0.000
## 2 -5.8860 0.9556               2 -243.484 491.0 20.51  0.000
## 1 -0.7653                      1 -249.988 502.0 31.50  0.000
## Models ranked by AICc(x)

The model selection table suggest that the full model is the most appropriate, at least considering the AIC.


  1. The script has been adapted from Prof. Paolo Girardi (A.Y. 2021/2022)↩︎

LS0tDQp0aXRsZTogIlN0YXRpc3RpY2FsIE1ldGhvZHMgYW5kIERhdGEgQW5hbHlzaXMgaW4gRGV2ZWxvcG1lbnRhbCBQc3ljaG9sb2d5Ig0Kc3VidGl0bGU6ICJMYWIgOCINCmF1dGhvcjogIkZpbGlwcG8gR2FtYmFyb3RhIg0Kb3V0cHV0OiANCiAgICBodG1sX2RvY3VtZW50Og0KICAgICAgICBjb2RlX2ZvbGRpbmc6IHNob3cNCiAgICAgICAgdG9jOiB0cnVlDQogICAgICAgIHRvY19mbG9hdDogdHJ1ZQ0KICAgICAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQpkYXRlOiAiVXBkYXRlZCBvbiBgciBTeXMuRGF0ZSgpYCINCi0tLQ0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICBtZXNzYWdlID0gRkFMU0UsDQogICAgICAgICAgICAgICAgICAgICAgd2FybmluZyA9IEZBTFNFLA0KICAgICAgICAgICAgICAgICAgICAgIGRldiA9ICJzdmciKQ0KYGBgDQoNCmBgYHtyIHBhY2thZ2VzLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KZGV2dG9vbHM6OmxvYWRfYWxsKCkgIyBpZiB1c2luZyB0aGUgcnByb2plY3QgZG93bG9hZGVkIGZyb20gdGhlIHNsaWRlcw0KIyBzb3VyY2UoInV0aWxzLWdsbS5SIikgIyBpZiB1c2luZyBhIHN0YW5kYXJkIHNldHVwDQpsaWJyYXJ5KGhlcmUpDQpsaWJyYXJ5KHRpZHlyKSAjIGZvciBkYXRhIG1hbmlwdWxhdGlvbg0KbGlicmFyeShkcGx5cikgIyBmb3IgZGF0YSBtYW5pcHVsYXRpb24NCmxpYnJhcnkoZ2dwbG90MikgIyBwbG90dGluZw0KbGlicmFyeShwZXJmb3JtYW5jZSkgIyBkaWFnbm9zdGljDQpsaWJyYXJ5KGNhcikgIyBnZW5lcmFsIHV0aWxpdGllcw0KbGlicmFyeShNdU1JbikgIyBtb2RlbCBzZWxlY3Rpb24NCmBgYA0KDQpgYGB7ciBvcHRpb25zLCBpbmNsdWRlID0gRkFMU0V9DQp0aGVtZV9zZXQodGhlbWVfbWluaW1hbChiYXNlX3NpemUgPSAxNSkpDQpgYGANCg0KYGBge3Igc2NyaXB0LCBlY2hvPUZBTFNFfQ0KIyMgTGluayBpbiBHaXRodWIgcmVwbw0KZG93bmxvYWR0aGlzOjpkb3dubG9hZF9saW5rKA0KICBsaW5rID0gZG93bmxvYWRfbGluaygiaHR0cHM6Ly9naXRodWIuY29tL3N0YXQtdGVhY2hpbmcvU01EQS0yMDIzL2Jsb2IvbWFzdGVyL2xhYnMvbGFiOC5SIiksDQogIGJ1dHRvbl9sYWJlbCA9ICJEb3dubG9hZCB0aGUgUiBzY3JpcHQiLA0KICBidXR0b25fdHlwZSA9ICJkYW5nZXIiLA0KICBoYXNfaWNvbiA9IFRSVUUsDQogIGljb24gPSAiZmEgZmEtc2F2ZSIsDQogIHNlbGZfY29udGFpbmVkID0gRkFMU0UNCikNCmBgYA0KDQoNCiMgT3ZlcnZpZXdeW1RoZSBzY3JpcHQgaGFzIGJlZW4gYWRhcHRlZCBmcm9tIFByb2YuIFBhb2xvIEdpcmFyZGkgKEEuWS4gMjAyMS8yMDIyKV0NCg0KV2UgYXJlIGdvbm5hIHdvcmsgd2l0aCB0aGUgYGFkbWlzc2lvbi5jc3ZgIGRhdGFzZXQgY29udGFpbmluZyAkbiA9IDQwMCQgc3R1ZGVudHMgZm9yIHRoZSBhZG1pc3Npb24gdG8gdGhlIFVDTEEgVW5pdmVyc2l0eS4gQSByZXNlYXJjaGVyIGlzIGludGVyZXN0ZWQgaW4gaG93IHZhcmlhYmxlcywgc3VjaCBhcyBgZ3JlYCAoR3JhZHVhdGUgUmVjb3JkIEV4YW0gc2NvcmVzKSwgYGdwYWAgKEdQQSksIGByYW5rYCAocHJlc3RpZ2Ugb2YgdGhlIHVuZGVyZ3JhZHVhdGUgaW5zdGl0dXRpb24pIGhhdmUgYW4gaW5mbHVlbmNlIGZvciB0aGUgYWRtaXNzaW9uIGludG8gZ3JhZHVhdGUgc2Nob29sLiBUaGUgcmVzcG9uc2UgdmFyaWFibGUsIGFkbWl0L2RvbuKAmXQgYWRtaXQsIGlzIGEgYmluYXJ5IHZhcmlhYmxlLg0KDQoxLiBJbXBvcnRpbmcgZGF0YSBhbmQgY2hlY2sNCjIuIEV4cGxvcmF0b3J5IGRhdGEgYW5hbHlzaXMNCjMuIE1vZGVsIGZpdHRpbmcgd2l0aCBgZ2xtKClgDQo0LiBNb2RlbCBkaWFnbm9zdGljDQo1LiBJbnRlcnByZXRpbmcgcGFyYW1ldGVycw0KNi4gTW9kZWwgc2VsZWN0aW9uDQoNCiMgMS4gSW1wb3J0aW5nIGRhdGENCg0KV2UgbmVlZCB0byBzZXQgdGhlICoqd29ya2luZyBkaXJlY3RvcnkqKiBvbiB0aGUgcm9vdCBvZiB0aGUgY291cnNlIGZvbGRlciB1c2luZyBgc2V0LndkKClgLiBVc2luZyBSIFByb2plY3RzIGlzIGp1c3QgbmVjZXNzYXJ5IHRvIG9wZW4gdGhlIGAuUlByb2pgIGZpbGUgYW5kIHRoZSB3b3JraW5nIGRpcmVjdG9yeSB3aWxsIGJlIGF1dG9tYXRpY2FsbHkgY29ycmVjdGx5IHNlbGVjdGVkLg0KDQpgYGB7cn0NCiMgcmVhZGluZyBkYXRhDQphZG1pc3Npb24gPC0gcmVhZC5jc3YoaGVyZSgiZGF0YSIsICJhZG1pc3Npb24uY3N2IiksIGhlYWRlcj1UUlVFKQ0KDQojIGZpcnN0IHJvd3MNCmhlYWQoYWRtaXNzaW9uKQ0KDQojIGNoZWNrIGRhdGFzZXQgc3RydWN0dXJlDQpzdHIoYWRtaXNzaW9uKQ0KDQojIHN1bW1hcnkgc3RhdGlzdGljcw0Kc3VtbWFyeShhZG1pc3Npb24pDQpgYGANCg0KSXQgaXMgdmVyeSBpbXBvcnRhbnQgdGhhdCBlYWNoIHZhcmlhYmxlIGlzIGNvcnJlY3RseSBpbnRlcnByZXRlZCBieSBSOg0KDQotIGBhZG1pdGAgaXMgYSBiaW5hcnkgdmFyaWFibGUgc3RvcmVkIGFzIGludGVnZXIgKDAgYW5kIDEpDQotIGBncmVgIGlzIGEgbnVtZXJpY2FsIHZhcmlhYmxlIHN0b3JlZCBhcyBpbnRlZ2VyDQotIGBncGFgIGlzIGEgbnVtZXJpY2FsIHZhcmlhYmxlcyBzdG9yZWQgYXMgZG91YmxlIHByZWNpc2lvbiBudW1iZXINCi0gYHJhbmtgIGlzIGEgbnVtZXJpY2FsIHZhcmlhYmxlcyBzdG9yZWQgYXMgaW50ZWdlcg0KDQpXZSBjb3VsZCBjaGFuZ2UgdGhlIHR5cGUgb2YgYHJhbmtgIHRvIGZhY3RvciBiZWNhdXNlIHdlIGFyZSBnb2luZyB0byB1c2UgaXQgYXMgYSBjYXRlZ29yaWNhbCAobWF5YmUgb3JkaW5hbCkgdmFyaWFibGUuDQoNCmBgYHtyfQ0KYWRtaXNzaW9uJHJhbmtjIDwtIGZhY3RvcihhZG1pc3Npb24kcmFuaywgbGV2ZWxzID0gMTo0LCBsYWJlbHMgPSAxOjQpDQpgYGANCg0KIyAyLiBFeHBsb3JhdG9yeSBkYXRhIGFuYWx5c2lzDQoNCldlIGNhbiBwbG90IHRoZSB1bml2YXJpYXRlIGRpc3RyaWJ1dGlvbiBvZiBlYWNoIHZhcmlhYmxlOg0KDQpgYGB7cn0NCiMgZ3JlIGFuZCBncGENCmFkbWlzc2lvbiB8PiANCiAgICBzZWxlY3QoZ3JlLCBncGEpIHw+IA0KICAgIHBpdm90X2xvbmdlcigxOjIpIHw+IA0KICAgIGdncGxvdChhZXMoeCA9IHZhbHVlKSkgKw0KICAgIGdlb21faGlzdG9ncmFtKGNvbCA9ICJibGFjayIsDQogICAgICAgICAgICAgICAgICAgZmlsbCA9ICJsaWdodGJsdWUiKSArDQogICAgZmFjZXRfd3JhcCh+bmFtZSwgc2NhbGVzID0gImZyZWUiKQ0KYGBgDQoNCmBgYHtyfQ0KYWRtaXNzaW9uIHw+IA0KICAgIGdncGxvdChhZXMoeCA9IHJhbmspKSArDQogICAgZ2VvbV9iYXIoKQ0KYGBgDQoNCmBgYHtyfQ0KYWRtaXNzaW9uIHw+IA0KICAgIGdncGxvdChhZXMoeCA9IGFkbWl0KSkgKw0KICAgIGdlb21fYmFyKCkNCmBgYA0KDQpUaGVuIHdlIGNhbiBjdXQgdGhlIGBncGFgIGFuZCBgZ3JlYCB2YXJpYWJpbGVzIGludG8gY2F0ZWdvcmllcyBhbmQgcGxvdCB0aGUgYWRtaXNzaW9ucyBmb3IgZWFjaCBiaW4gKGkuZS4sIGEgY29udGluZ2VuY3kgdGFibGUpOg0KDQpgYGB7cn0NCmFkbWlzc2lvbiRncGFfYyA8LSBjdXQoYWRtaXNzaW9uJGdwYSwgc2VxKDQsIDYsIDAuMiksIGxhYmVscyA9IEZBTFNFKQ0KYWRtaXNzaW9uJGdyZV9jIDwtIGN1dChhZG1pc3Npb24kZ3JlLCBzZXEoMjYwLCA5NjAsIDUwKSwgbGFiZWxzPUZBTFNFKQ0KYGBgDQoNCmBgYHtyfQ0KIyBhZG1pc3Npb24gfiBncGENCmFkbWlzc2lvbiB8PiANCiAgICBnZ3Bsb3QoYWVzKHggPSBncGFfYywgZmlsbCA9IGZhY3RvcihhZG1pdCkpKSArDQogICAgZ2VvbV9iYXIocG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgpKSArDQogICAgbGFicyhmaWxsID0gIkFkbWlzc2lvbiIpICsNCiAgICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIikNCg0KIyBhZG1pc3Npb24gfiBncmUNCmFkbWlzc2lvbiB8PiANCiAgICBnZ3Bsb3QoYWVzKHggPSBncmVfYywgZmlsbCA9IGZhY3RvcihhZG1pdCkpKSArDQogICAgZ2VvbV9iYXIocG9zaXRpb24gPSBwb3NpdGlvbl9kb2RnZSgpKSArDQogICAgbGFicyhmaWxsID0gIkFkbWlzc2lvbiIpICsNCiAgICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAiYm90dG9tIikNCmBgYA0KDQpHaXZlbiB0aGF0IHRoZSBudW1iZXIgb2YgYWRtaXR0ZWQgaXMgbG93ZXIgdGhhbiB0aGUgbnVtYmVyIG9mIG5vbiBhZG1pdHRlZCwgd2UgY2FuIGhhdmUgYSBsb29rIGF0IHRoZSBwcm9wb3J0aW9uIG9mIGFkbWlzc2lvbiBmb3IgZWFjaCBiaW46DQoNCmBgYHtyfQ0KYWRtaXNzaW9uIHw+IA0KICAgIGdyb3VwX2J5KGdwYV9jKSB8PiANCiAgICBzdW1tYXJpc2UoYWRtaXQgPSBtZWFuKGFkbWl0KSwNCiAgICAgICAgICAgICAgbm9uX2FkbWl0ID0gMSAtIGFkbWl0KSB8PiANCiAgICBwaXZvdF9sb25nZXIoMjozKSB8PiANCiAgICBnZ3Bsb3QoYWVzKHggPSBmYWN0b3IoZ3BhX2MpLCB5ID0gdmFsdWUsIGZpbGwgPSBuYW1lKSkgKw0KICAgIGdlb21fY29sKCkgKw0KICAgIGxhYnMoZmlsbCA9ICJBZG1pc3Npb24iLA0KICAgICAgICAgeSA9ICIlIiwNCiAgICAgICAgIHggPSAiZ3BhIikgKw0KICAgIHRoZW1lKGxlZ2VuZC5wb3NpdGlvbiA9ICJib3R0b20iKQ0KDQphZG1pc3Npb24gfD4gDQogICAgZ3JvdXBfYnkoZ3JlX2MpIHw+IA0KICAgIHN1bW1hcmlzZShhZG1pdCA9IG1lYW4oYWRtaXQpLA0KICAgICAgICAgICAgICBub25fYWRtaXQgPSAxIC0gYWRtaXQpIHw+IA0KICAgIHBpdm90X2xvbmdlcigyOjMpIHw+IA0KICAgIGdncGxvdChhZXMoeCA9IGZhY3RvcihncmVfYyksIHkgPSB2YWx1ZSwgZmlsbCA9IG5hbWUpKSArDQogICAgZ2VvbV9jb2woKSArDQogICAgbGFicyhmaWxsID0gIkFkbWlzc2lvbiIsDQogICAgICAgICB5ID0gIiUiLA0KICAgICAgICAgeCA9ICJncGEiKSArDQogICAgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gImJvdHRvbSIpDQpgYGANCg0KRmluYWxseSB3ZSBjYW4gaGF2ZSBhIGxvb2sgYXQgdGhlIGFkbWlzc2lvbnMgYXMgYSBmdW5jdGlvbiBvZiB0aGUgcmFuayBvZiB0aGUgdW5kZXJncmFkIGluc3RpdHV0aW9uOg0KDQpgYGB7cn0NCiMgbWFyZ2luID0gMiBtZWFucyB0aGF0IGVhY2ggY29sdW0gd2lsbCBzdW0gdG8gMQ0KcHJvcC50YWJsZSh0YWJsZShhZG1pc3Npb24kYWRtaXQsIGFkbWlzc2lvbiRyYW5rKSwgbWFyZ2luID0gMikNCmBgYA0KDQpDbGVhcmx5IGFzIHRoZSByYW5rIG9mIHRoZSBpbnN0aXR1dGUgZGVjcmVhc2UgKGZyb20gMSB0byA0KSBhbHNvIHRoZSBwcm9wb3J0aW9ucyBvZiBhZG1pc3Npb25zIGRlY3JlYXNlLg0KDQojIDMuIE1vZGVsIGZpdHRpbmcgd2l0aCBgZ2xtKClgDQoNCk5vdyB3ZSBjYSBmaXQgdGhlIG1vZGVsIHVzaW5nIGBnbG0oKWAuIExldCdzIHN0YXJ0IGJ5IGZpdHRpbmcgYSAqbnVsbCogbW9kZWwgd2l0aCBubyBwcmVkaWN0b3JzLiBXZSBjaG9vc2UgYSBiaW5vbWlhbCBgZ2xtYCB3aXRoIGEgKipsb2dpdCoqIGxpbmsgZnVuY3Rpb24uDQoNCmBgYHtyfQ0KZml0MCA8LSBnbG0oYWRtaXQgfiAxLCBkYXRhID0gYWRtaXNzaW9uLCBmYW1pbHkgPSBiaW5vbWlhbChsaW5rID0gImxvZ2l0IikpDQpzdW1tYXJ5KGZpdDApDQpgYGANCg0KVGhlbiB3ZSBjYW4gZml0IHRoZSBmdWxsIG1vZGVsIGJ5IHB1dHRpbmcgYWxsIHByZWRpY3RvcnM6DQoNCmBgYHtyfQ0KZml0MSA8LSBnbG0oYWRtaXQgfiBncmUgKyBncGEgKyByYW5rYywgZmFtaWx5ID0gYmlub21pYWwobGluayA9ICJsb2dpdCIpLCBkYXRhID0gYWRtaXNzaW9uKQ0Kc3VtbWFyeShmaXQxKQ0KYGBgDQoNCiMgNC4gTW9kZWwgZGlhZ25vc3RpYw0KDQpGaXJzdGx5IHdlIGNhbiBoYXZlIGEgbG9vayB0byB0aGUgYHJlc2lkdWFsIH4gZml0dGVkYCBwbG90Og0KDQpgYGB7cn0NCnBsb3RfcmVzaWQoZml0MSwgdHlwZSA9ICJwZWFyc29uIikNCmBgYA0KDQpHaXZlbiB0aGF0IHRoZSBgYWRtaXRgIGlzIGEgYmluYXJ5IHZhcmlhYmxlcyBhbmQgd2UgYXJlIHVzaW5nIGEgYmVybm91bGxpIG1vZGVsIHdlIGNhbiB1c2UgdGhlICoqYmlubmVkIHJlc2lkdWFscyoqIHRvIGhhdmUgYSBiZXR0ZXIgaWRlYToNCg0KYGBge3J9DQpiaW5yZXMgPC0gZGF0YS5mcmFtZShwZXJmb3JtYW5jZTo6YmlubmVkX3Jlc2lkdWFscyhmaXQxLCBuX2JpbnMgPSAyMCkpDQoNCmJpbnJlcyB8PiANCiAgICBnZ3Bsb3QoYWVzKHggPSB4YmFyLCB5ID0geWJhcikpICsNCiAgICBnZW9tX3BvaW50KCkgKw0KICAgIGdlb21fbGluZShhZXMoeCA9IHhiYXIsIHkgPSAyKnNlKSkgKw0KICAgIGdlb21fbGluZShhZXMoeCA9IHhiYXIsIHkgPSAtMipzZSkpICsNCiAgICB5bGltKGMoLTAuNSwwLjUpKSArDQogICAgeGxhYigiQmlubmVkIGZpdHRlZChmaXQpIikgKw0KICAgIHlsYWIoIkJpbm5lZCByZXNpZHVhbHMoZml0KSIpDQpgYGANCg0KVGhlbiB3ZSBjYW4gY2hlY2sgZWFjaCBwcmVkaWN0b3JzIGFzIGEgZnVuY3Rpb24gb2YgcmVzaWR1YWxzOg0KDQpgYGB7ciwgZmlnLndpZHRoPTEwLCBmaWcuaGVpZ2h0PTEwfQ0KcmVzaWR1YWxQbG90cyhmaXQxLCB0ZXN0cyA9IEZBTFNFKQ0KYGBgDQoNClRoZW4gd2UgY2FuIGNoZWNrIGZvciBpbmZsdWVudGlhbCBvYnNlcnZhdGlvbnM6DQoNCmBgYHtyfQ0KaW5mbCA8LSBpbmZsX21lYXN1cmUoZml0MSkNCmhlYWQoaW5mbCkNCmBgYA0KDQpQbG90dGluZyB1c2luZyBgY2FyYA0KDQpgYGB7cn0NCmNhcjo6aW5mbHVlbmNlSW5kZXhQbG90KGZpdDEsIHZhcnMgPSBjKCJTdHVkZW50aXplZCIsICJoYXQiLCAiQ29vayIpKQ0KYGBgDQoNClBsb3R0aW5nIGFsc28gdGhlIGRmYmV0YToNCg0KYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD0xMH0NCmRmYmV0YV9wbG90KGZpdDEpDQpgYGANCg0KQ2hlY2sgaWYgdGhlcmUgYXJlIG9ic2VydmF0aW9ucyB3aXRoIGhpZ2ggc3RhbmRhcmRpemVkIChzdHVkZW50aXplZCkgcmVzaWR1YWxzOg0KDQpgYGB7cn0NCm91dGxpZXJUZXN0KGZpdDEpICMgVGVzdGluZyBvdXRsaWVycw0KYGBgDQoNCkZvciBwb3RlbnRpYWxseSBpbmZsdWVudGlhbCBvYnNlcnZhdGlvbnMgd2UgY291bGQgZmlyIGEgbW9kZWwgc3VidHJhY3RpbmcgdGhhdCBzcGVjaWZpYyBvYnNlcnZhdGlvbiBhbmQgY29tcGFyZSBjb2VmZmljaWVudHMuIFRoaXMgaXMgc2ltaWxhciB0byB0aGUgZGZiZXRhIG1ldHJpYyB0aGF0IHN1Z2dlc3Qgbm8gaW5mbHVlbnRpYWwgb2JzZXJ2YXRpb25zIG9uIG1vZGVsIHBhcmFtZXRlcnMuDQoNCmBgYHtyfQ0KIyBJcyAxOTggcmVhbGx5IGluZmx1ZW50aWFsPw0KZml0MV9ubzE5OCA8LSB1cGRhdGUoZml0MSwgc3Vic2V0PS1jKDE5OCkpDQpjb21wYXJlQ29lZnMoZml0MSwgZml0MV9ubzE5OCkNCmBgYA0KDQojIDUuIEludGVycHJldGluZyBwYXJhbWV0ZXJzDQoNCkZpcnN0bHksIHdlIGNhbiBleHRyYWN0IG1vZGVsIHBhcmFtZXRlcnMsIHRha2luZyB0aGUgZXhwb25lbnRpYWwgdG8gaW50ZXJwcmV0IHRoZW0gYXMgb2RkcyByYXRpb3M6DQoNCmBgYHtyfQ0KYnJvb206OnRpZHkoZml0MSwgZXhwb25lbnRpYXRlID0gVFJVRSwgY29uZi5pbnQgPSBUUlVFKQ0KYGBgDQoNCldlIGNhbiBpbnRlcnByZXQgdGhlc2UgcGFyYW1ldGVycyBhczogZm9yIGEgdW5pdCBpbmNyZWFzZSBpbiB0aGUgYHhgLCB0aGUgb2RkcyBvZiBiZWluZyBhY2NlcHRlZCBpbiBncmFkIHNjaG9vbCBpbmNyZWFzZSBieSBgZXhwKGJldGEpYC4gSWYgd2UgbXVsdGlwbHkgdGhlIGBleHAoYmV0YSkqMTAwYCB3ZSBvYnRhaW4gdGhlIGV4cGVjdGVkIGluY3JlYXNlIGluIHBlcmNlbnRhZ2UuIEdpdmVuIHRoYXQgd2UgaGF2ZSBtdWx0aXBsZSBwYXJhbWV0ZXJzLCB3aGVuIHdlIGludGVwcmV0IGEgc3BlY2lmaWMgcGFyYW1ldGVyIHdlIGFyZSBjb250cm9sbGluZyBmb3Igb3RoZXIgcGFyYW1ldGVycy4NCg0KYGBge3J9DQpicm9vbTo6dGlkeShmaXQxLCBleHBvbmVudGlhdGUgPSBUUlVFLCBjb25mLmludCA9IFRSVUUpIHw+DQogICAgc2xpY2UoLTEpIHw+IA0KICAgIG11dGF0ZShlc3RwZXJjID0gZXN0aW1hdGUgKiAxMDApDQpgYGANCg0KVG8gYmV0dGVyIGludGVycHJldCB0aGUgcGFyYW1ldGVycyB3ZSBuZWVkIHRvIG1ha2Ugc3VyZSB0aGF0IHRoZSBzY2FsZSBpcyBtZWFuaW5nZnVsLiBGb3IgZXhhbXBsZSwgdGhlIGBncmVgIGVmZmVjdCBzZWVtcyB0byBiZSB2ZXJ5IHNtYWxsIGJ1dCBzdGF0aXN0aWNhbGx5IHNpZ25pZmljYW50LiBUaGUgcmVhc29uIGlzIHRoYXQgYSB1bml0IGluY3JlYXNlIGluIGBncmVgIGlzIHZlcnkgc21hbGwuIFdlIGNvdWxkIGZvciBleGFtcGxlIHJlc2NhbGUgdGhlIHZhcmlhYmxlIGRpdmlkaW5nIGZvciBhIGNvbnN0YW50IHRlcm06DQoNCmBgYHtyfQ0KcGFyKG1mcm93ID0gYygxLDIpKQ0KaGlzdChhZG1pc3Npb24kZ3JlKQ0KaGlzdChhZG1pc3Npb24kZ3JlLzEwMCkNCmBgYA0KDQpMZXQncyB0cnkgZml0dGluZyB0aGUgbW9kZWwgd2l0aCB0aGUgbmV3IHZhcmlhYmxlOg0KDQpgYGB7cn0NCmFkbWlzc2lvbiRncmUxMDAgPC0gYWRtaXNzaW9uJGdyZS8xMDANCmZpdDIgPC0gZ2xtKGFkbWl0IH4gZ3JlMTAwICsgZ3BhICsgcmFua2MsIGZhbWlseSA9IGJpbm9taWFsKGxpbmsgPSAibG9naXQiKSwgZGF0YSA9IGFkbWlzc2lvbikNCg0Kc3VtbWFyeShmaXQyKQ0KYGBgDQoNCmBgYHtyfQ0KYnJvb206OnRpZHkoZml0MiwgZXhwb25lbnRpYXRlID0gVFJVRSwgY29uZi5pbnQgPSBUUlVFKSB8Pg0KICAgIHNsaWNlKC0xKSB8PiANCiAgICBtdXRhdGUoZXN0cGVyYyA9IGVzdGltYXRlICogMTAwKQ0KYGBgDQoNCk5vdyB0aGUgYGdyZWAgZWZmZWN0IGlzIG1vcmUgbWVhbmluZ2Z1bC4gTm90aWNlIGhvdyB0aGUgb3ZlcmFsbCBtb2RlbCBmaXR0aW5nIGlzIG5vdCBjaGFuZ2VkIHRvZ2hldGVyIHdpdGggb3RoZXIgcGFyYW1ldGVycy4gV2UgYXJlIG9ubHkgcmVzY2FsaW5nIHZhcmlhYmxlcy4NCg0KR2VuZXJhbGx5IHdlIGNhbiBwbG90IHRoZSBlZmZlY3RzIGZvciBhIGJldHRlciBvdmVydmlldyBvZiB0aGUgbW9kZWw6DQoNCmBgYHtyfQ0KcGxvdChlZmZlY3RzOjphbGxFZmZlY3RzKGZpdDEpKQ0KYGBgDQoNClRvIGludGVycHJldCB0aGUgcGFyYW1ldGVycyBpbiBwcm9iYWJpbGl0eSB0ZXJtcyB3ZSBjb3VsZCB1c2UgdGhlIGRpdmlkZSBieSA0IHJ1bGUgdGhhdCBleHByZXNzIHRoZSBtYXhpbXVtIHNsb3BlIChpLmUuLCB0aGUgbWF4aW11bSBwcm9iYWJpbGl0eSBpbmNyZWFzZSk6DQoNCmBgYHtyfQ0KY29lZihmaXQyKVstMV0vNA0KYGBgDQoNClNpbWlsYXJseSB3ZSBjYW4gY29tcHV0ZSB0aGUgbWFyZ2luYWwgZWZmZWN0cyBmb3IgZWFjaCB2YXJpYWJsZSB0aGF0IHJlcHJlc2VudHMgdGhlIGF2ZXJhZ2Ugc2xvcGU6DQoNCmBgYHtyfQ0KbWFyZ2luczo6bWFyZ2lucyhmaXQyKSB8PiBzdW1tYXJ5KCkNCmBgYA0KDQpCZXlvbmQgdGhlIG1vZGVsIGNvZWZmaWNpZW50cywgd2UgY291bGQgdXNlIGEgbGlrZWxpaG9vZCByYXRpbyB0ZXN0LiBMZXQncyBzdGFydCBieSBjb21wYXJpbmcgdGhlIG51bGwgbW9kZWwgd2l0aCB0aGUgY3VycmVudCBtb2RlbC4gV2UgaG9wZSB0aGF0IG91ciB2YXJpYWJsZXMgY29tYmluYXRpb25zIGFyZSBkb2luZyBhIGJldHRlciBqb2IgY29tcGFyZWQgdG8gYSBudWxsIG1vZGVsOg0KDQpgYGB7cn0NCmFub3ZhKGZpdDAsIGZpdDEsIHRlc3QgPSAiTFJUIikNCmBgYA0KDQpBcyBleHBlY3RlZCBmcm9tIG1vZGVsIHN1bW1hcnkgYW5kIHRoZSBkZXZpYW5jZSByZWR1Y3Rpb24sIHRoZSB2YXJpYWJsZXMgYXJlIHVzZWZ1bCB0byBwcmVkaWN0IHRoZSBwcm9iYWJpbGl0eSBvZiBhZG1pc3Npb24uIEhvdyB1c2VmdWw/IHdlIGNvdWxkIHVzZSBzb21lICRSXjIkLWxpa2UgbWVhc3VyZXM6DQoNCmBgYHtyfQ0KcGVyZm9ybWFuY2U6OnIyX3RqdXIoZml0MSkNCmBgYA0KDQpEZXNwaXRlIHVzZWZ1bCwgdGhlIG1vZGVsIGhhcyBhIGxvdyAkUl4yJC4gRnVydGhlcm1vcmUgdGhlIGNvcnJlY3QgY2xhc3NpZmljYXRpb24gcmF0ZSBpcyBoaWdoZXIgdGhhbiB0aGUgY2hhbmNlIGxldmVsIGJ1dCByZWxhdGl2ZWx5IGxvdzoNCg0KYGBge3J9DQoxIC0gZXJyb3JfcmF0ZShmaXQxKQ0KYGBgDQoNCiMgNy4gTW9kZWwgc2VsZWN0aW9uDQoNCldlIGNvdWxkIHRyeSBhIG1vZGVsIGNvbXBhcmlzb24gc3RhcnRpbmcgZnJvbSB0aGUgbnVsbCBtb2RlbCBhbmQgZmluaXNoaW5nIHRvIHRoZSBvdmVyYWxsIG1vZGVsOg0KDQpgYGB7cn0NCmZpdDIgPC0gdXBkYXRlKGZpdDIsIG5hLmFjdGlvbiA9IG5hLmZhaWwpICMgcmVxdWlyZWQgZm9yIG11bWluDQpkcmVkZ2UoZml0MikNCmBgYA0KDQpUaGUgbW9kZWwgc2VsZWN0aW9uIHRhYmxlIHN1Z2dlc3QgdGhhdCB0aGUgZnVsbCBtb2RlbCBpcyB0aGUgbW9zdCBhcHByb3ByaWF0ZSwgYXQgbGVhc3QgY29uc2lkZXJpbmcgdGhlIEFJQy4NCg==