In this example, we are going to learn about an alternative method to encode text data known as word embeddings. This is an incomplete tutorial on word embeddings but will at least give you the basic understanding on when, why, and how we use them.

Learning objectives:

  • What word embeddings are.
  • The two main contexts that word embeddings are trained.
  • When we should use word embeddings.
  • How to train word embeddings for classification purposes.

Requirements

# Initialize package
library(keras)
library(fs)
library(tidyverse)
library(glue)
library(progress)

# helper functions we'll use to explore word embeddings
source("helper_functions.R")

The “real” IMDB dataset

Keras provides a built-in IMBD dataset dataset_imdb(), which contains the text of 25,000 movie reviews that have been classified as net positive or net negative. However, we are going to use the original IMDB movie review files which can be found at http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz. This tends to help students better understand the entire data prep required for text.

You can find the download instructions here. For those in the workshop we have already downloaded this data for you.

if (stringr::str_detect(here::here(), "conf-2020-user")) {
  imdb_dir <- "/home/conf-2020-user/data/imdb"
} else {
  imdb_dir <- here::here("materials", "data", "imdb")
}
fs::dir_tree(imdb_dir, type = "directory")
/Users/b294776/Desktop/Workspace/Training/rstudio-conf-2020/dl-keras-tf/materials/data/imdb
├── test
│   ├── neg
│   └── pos
└── train
    ├── neg
    └── pos

You can see the data have already been separated into test vs training sets and positive vs negative sets. The actual reviews are contained in individual .txt files. We can use this structure to our advantage - the below iterates over each review and

  1. creates the path to each individual review file,
  2. creats a label based on the “neg” or “pos” folder the review is in,
  3. and saves the output as a data frame with each review on an individual row.
training_files <- file.path(imdb_dir, "train") %>%
  dir_ls(type = "directory") %>%
  map(dir_ls) %>%
  set_names(basename) %>%
  plyr::ldply(data_frame) %>%
  set_names(c("label", "path"))

training_files

We can see our response observations are balanced:

count(training_files, label)

We can now iterate over each row and

  1. save the label in a labels vector,
  2. import the movie review and
  3. save in a texts vector.
obs <- nrow(training_files)
labels <- vector(mode = "integer", length = obs)
texts <- vector(mode = "character", length = obs)

# this just allows us to track progress of our loop
pb <- progress_bar$new(total = obs, width = 60)

for (file in seq_len(obs)) {
  pb$tick()
  
  label <- training_files[[file, "label"]]
  path <- training_files[[file, "path"]]
  
  labels[file] <- ifelse(label == "neg", 0, 1)
  texts[file] <- readChar(path, nchars = file.size(path)) 
  
}

We now have two vectors, one consisting of the labels…

table(labels)
labels
    0     1 
12500 12500 

and the other holding each review.

texts[1]
[1] "Story of a man who has unnatural feelings for a pig. Starts out with a opening scene that is a terrific example of absurd comedy. A formal orchestra audience is turned into an insane, violent mob by the crazy chantings of it's singers. Unfortunately it stays absurd the WHOLE time with no general narrative eventually making it just too off putting. Even those from the era should be turned off. The cryptic dialogue would make Shakespeare seem easy to a third grader. On a technical level it's better than you might think with some good cinematography by future great Vilmos Zsigmond. Future stars Sally Kirkland and Frederic Forrest can be seen briefly."

Exploratory text analysis

A little exploratory analysis will show us the total number of unique words across our corpus and the average length of each review. Its good to know the word count distribution of your text as later on we’ll make a decision of how many words to keep.

text_df <- texts %>%
  tibble(.name_repair = ~ "text") %>%
  mutate(text_length = str_count(text, "\\w+"))

unique_words <- text_df %>%
  tidytext::unnest_tokens(word, text) %>%
  pull(word) %>%
  n_distinct()

avg_review_length <- median(text_df$text_length, na.rm = TRUE)
  
ggplot(text_df, aes(text_length)) +
  geom_histogram(bins = 100, fill = "grey70", color = "grey40") +
  geom_vline(xintercept = avg_review_length, color = "red", lty = "dashed") +
  scale_x_log10("# words") +
  ggtitle(glue("Median review length is {avg_review_length} words"),
          subtitle = glue("Total number of unique words is {unique_words}"))

Word embeddings for language modeling

Word embeddings are designed to encode general semantic relationships which can serve two principle purposes. The first is for language modeling which aims to encode words for the purpose of predicting synonyms, sentence completion, and word relationships. ℹ️

Although we are not focusing on word embeddings for this purpose, I have written a couple helper functions to train word embeddings for this purpose. See the code behind these helper functions here.

# clean up text and compute word embeddings
clean_text <- tolower(texts) %>%
  str_replace_all(pattern = "[[:punct:] ]+", replacement = " ") %>%
  str_trim()

word_embeddings <- get_embeddings(clean_text)
Creating vocabulary...
Creating term-co-occurence matrix...
Computing embeddings based on GloVe algorithm...
INFO [2019-12-09 09:14:04] 2019-12-09 09:14:04 - epoch 1, expected cost 0.0821
INFO [2019-12-09 09:14:05] 2019-12-09 09:14:05 - epoch 2, expected cost 0.0555
INFO [2019-12-09 09:14:06] 2019-12-09 09:14:06 - epoch 3, expected cost 0.0485
INFO [2019-12-09 09:14:07] 2019-12-09 09:14:07 - epoch 4, expected cost 0.0443
INFO [2019-12-09 09:14:08] 2019-12-09 09:14:08 - epoch 5, expected cost 0.0415
INFO [2019-12-09 09:14:09] 2019-12-09 09:14:09 - epoch 6, expected cost 0.0395
INFO [2019-12-09 09:14:11] 2019-12-09 09:14:11 - epoch 7, expected cost 0.0379
INFO [2019-12-09 09:14:12] 2019-12-09 09:14:12 - epoch 8, expected cost 0.0367
INFO [2019-12-09 09:14:13] 2019-12-09 09:14:13 - epoch 9, expected cost 0.0357
INFO [2019-12-09 09:14:14] 2019-12-09 09:14:14 - epoch 10, expected cost 0.0348
INFO [2019-12-09 09:14:15] 2019-12-09 09:14:15 - epoch 11, expected cost 0.0341
INFO [2019-12-09 09:14:16] 2019-12-09 09:14:16 - epoch 12, expected cost 0.0335
INFO [2019-12-09 09:14:17] 2019-12-09 09:14:17 - epoch 13, expected cost 0.0330
INFO [2019-12-09 09:14:18] 2019-12-09 09:14:18 - epoch 14, expected cost 0.0326
INFO [2019-12-09 09:14:19] 2019-12-09 09:14:19 - epoch 15, expected cost 0.0322
INFO [2019-12-09 09:14:20] 2019-12-09 09:14:20 - epoch 16, expected cost 0.0318
INFO [2019-12-09 09:14:22] 2019-12-09 09:14:22 - epoch 17, expected cost 0.0315
INFO [2019-12-09 09:14:23] 2019-12-09 09:14:23 - epoch 18, expected cost 0.0312
INFO [2019-12-09 09:14:23] Success: early stopping. Improvement at iterartion 18 is less then convergence_tol

Explore your own words!

# find words with similar embeddings
get_similar_words("horrible", word_embeddings)
 horrible  terrible     awful       bad    acting 
1.0000000 0.9132471 0.8663343 0.8041792 0.7790165 

Word embeddings for classification

The other principle purpose for word embeddings is to encode text for classification reasons. In this case, we train the word embeddings to take on weights that optimize the classification loss function. ℹ️

Prepare data

Our response variable labels is already a tensor; however, we still need to preprocess our text features. To do so we:

  1. Specify how many words we want to include. This example uses the 10,000 words with the highest usage (frequency).
  2. Create a text_tokenizer object which defines how we want to preprocess the text (i.e. convert to lowercase, remove punctuation, token splitting characters). For the most part, the defaults are sufficient.
  3. Apply the tokenizer to our text with fit_text_tokenizer. This results in an object with many details of our corpus (i.e. word counts, word index).
top_n_words <- 10000

tokenizer <- text_tokenizer(num_words = top_n_words) %>% 
  fit_text_tokenizer(texts)

names(tokenizer)
 [1] "char_level"                   "document_count"               "filters"                     
 [4] "fit_on_sequences"             "fit_on_texts"                 "get_config"                  
 [7] "index_docs"                   "index_word"                   "lower"                       
[10] "num_words"                    "oov_token"                    "sequences_to_matrix"         
[13] "sequences_to_texts"           "sequences_to_texts_generator" "split"                       
[16] "texts_to_matrix"              "texts_to_sequences"           "texts_to_sequences_generator"
[19] "to_json"                      "word_counts"                  "word_docs"                   
[22] "word_index"                  

We have now tokenized our reviews. We are considering 10,000 of 88,582 total unique words. The most common words include:

head(tokenizer$word_index)
$the
[1] 1

$and
[1] 2

$a
[1] 3

$of
[1] 4

$to
[1] 5

$is
[1] 6

Next, we extract our vectorized review data as a list. Each review is encoded as a sequence of word indexes (integers).

sequences <- texts_to_sequences(tokenizer, texts)

# The vectorized first instance:
sequences[[1]]
  [1]   62    4    3  129   34   44 7576 1414   15    3 4252  514   43   16    3  633
 [17]  133   12    6    3 1301  459    4 1751  209    3 7693  308    6  676   80   32
 [33] 2137 1110 3008   31    1  929    4   42 5120  469    9 2665 1751    1  223   55
 [49]   16   54  828 1318  847  228    9   40   96  122 1484   57  145   36    1  996
 [65]  141   27  676  122    1  411   59   94 2278  303  772    5    3  837   20    3
 [81] 1755  646   42  125   71   22  235  101   16   46   49  624   31  702   84  702
 [97]  378 3493    2 8422   67   27  107 3348

We can map the integer values back to the word index. The integer number corresponds to the position in the word count list and the name of the vector is the actual word.

paste(unlist(tokenizer$index_word)[sequences[[1]]] , collapse = " ")
[1] "story of a man who has unnatural feelings for a pig starts out with a opening scene that is a terrific example of absurd comedy a orchestra audience is turned into an insane violent mob by the crazy of it's singers unfortunately it stays absurd the whole time with no general narrative eventually making it just too off putting even those from the era should be turned off the dialogue would make shakespeare seem easy to a third on a technical level it's better than you might think with some good cinematography by future great future stars sally and forrest can be seen briefly"

We can see how our tokenizer converted our original text to a cleaned up version:

cat("Original text:\n")
Original text:
texts[[1]]
[1] "Story of a man who has unnatural feelings for a pig. Starts out with a opening scene that is a terrific example of absurd comedy. A formal orchestra audience is turned into an insane, violent mob by the crazy chantings of it's singers. Unfortunately it stays absurd the WHOLE time with no general narrative eventually making it just too off putting. Even those from the era should be turned off. The cryptic dialogue would make Shakespeare seem easy to a third grader. On a technical level it's better than you might think with some good cinematography by future great Vilmos Zsigmond. Future stars Sally Kirkland and Frederic Forrest can be seen briefly."
cat("\nRevised text:\n")

Revised text:
paste(unlist(tokenizer$index_word)[sequences[[1]]] , collapse = " ")
[1] "story of a man who has unnatural feelings for a pig starts out with a opening scene that is a terrific example of absurd comedy a orchestra audience is turned into an insane violent mob by the crazy of it's singers unfortunately it stays absurd the whole time with no general narrative eventually making it just too off putting even those from the era should be turned off the dialogue would make shakespeare seem easy to a third on a technical level it's better than you might think with some good cinematography by future great future stars sally and forrest can be seen briefly"

Next, since each review is a different length, we need to limit ourselves to a certain number of words so that all our features (reviews) are the same length. This should be viewed as a tuning parameter.

Tip: I typically start with values around the 50% (median) but then explore values that represent 25% & 75% percentile of the word distribution when tuning.

Note (?pad_sequences):

  • Any reviews that are shorter than this length will be padded.
  • Any reviews that are longer than this length will be truncated.
max_len <- 150
features <- pad_sequences(sequences, maxlen = max_len)

Since this review includes less than 150 words from our word index of 10K most frequent words, it pads the front-end with zeros (see ?pad_sequences() for alternative padding options).

features[1,]
  [1]    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0
 [17]    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0    0
 [33]    0    0    0    0    0    0    0    0    0    0    0    0    0    0   62    4
 [49]    3  129   34   44 7576 1414   15    3 4252  514   43   16    3  633  133   12
 [65]    6    3 1301  459    4 1751  209    3 7693  308    6  676   80   32 2137 1110
 [81] 3008   31    1  929    4   42 5120  469    9 2665 1751    1  223   55   16   54
 [97]  828 1318  847  228    9   40   96  122 1484   57  145   36    1  996  141   27
[113]  676  122    1  411   59   94 2278  303  772    5    3  837   20    3 1755  646
[129]   42  125   71   22  235  101   16   46   49  624   31  702   84  702  378 3493
[145]    2 8422   67   27  107 3348

So, in essence, we have created an input that is a numeric representation of this:

features[1,] %>% 
  map_chr(~ ifelse(.x == 0, "<pad>", unlist(tokenizer$index_word[.x]))) %>%
  cat()
<pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> <pad> story of a man who has unnatural feelings for a pig starts out with a opening scene that is a terrific example of absurd comedy a orchestra audience is turned into an insane violent mob by the crazy of it's singers unfortunately it stays absurd the whole time with no general narrative eventually making it just too off putting even those from the era should be turned off the dialogue would make shakespeare seem easy to a third on a technical level it's better than you might think with some good cinematography by future great future stars sally and forrest can be seen briefly

Your Turn!

Check out different reviews and see how we have transformed the data. Remove eval=FALSE to run.

# use review number (i.e. 2, 10, 150)
which_review <- ____
  
cat(crayon::blue("Original text:\n"))
texts[[which_review ]]

cat(crayon::blue("\nRevised text:\n"))
paste(unlist(tokenizer$index_word)[features[which_review ,]] , collapse = " ")

cat(crayon::blue("\nEncoded text:\n"))
features[which_review,] %>% 
  map_chr(~ ifelse(.x == 0, "<pad>", unlist(tokenizer$index_word[.x]))) %>%
  cat()

Our data is now preprocessed! We have 25000 observations and 150 features. Our features data is a matrix where each row is a single observation and each column represents the words in the review in the order that they appear.

dim(features)
[1] 25000   150
length(labels)
[1] 25000

Model training

To train our model we will use the validation_split procedure within fit. Remember, this takes the last XX% of our data to be used as our validation set. But if you recall, our data was organized in neg and pos folders so we should randomize our data to make sure our validation set doesn’t end up being all positive or negative reviews!

set.seed(123)
index <- sample(1:nrow(features))

x_train <- features[index, ]
y_train <- labels[index]

To create our network architecture that includes word embeddings, we need to include two things:

  1. layer_embedding layer that creates the embeddings,
  2. layer_flatten to flatten our embeddings to a 2D tensor for our densely connected portion of our model

Note:

  • input_dim & input_length are considered pre-processing hyperparameters.
  • output_dim is our word embeddings hyperparameter.
  • They all have interaction effects
model <- keras_model_sequential() %>%
  layer_embedding(
    input_dim = top_n_words,   # number of words we are considering
    input_length = max_len,    # length that we have set each review to
    output_dim = 32            # length of our word embeddings
    ) %>%  
  layer_flatten() %>% 
  layer_dense(units = 1, activation = "sigmoid")

summary(model)
Model: "sequential_1"
_______________________________________________________________________________________
Layer (type)                           Output Shape                      Param #       
=======================================================================================
embedding_1 (Embedding)                (None, 150, 32)                   320000        
_______________________________________________________________________________________
flatten_1 (Flatten)                    (None, 4800)                      0             
_______________________________________________________________________________________
dense_1 (Dense)                        (None, 1)                         4801          
=======================================================================================
Total params: 324,801
Trainable params: 324,801
Non-trainable params: 0
_______________________________________________________________________________________

The rest of our modeling procedure follows the same protocols that you’ve seen in the other modules.

model %>% compile(
  optimizer = "rmsprop",
  loss = "binary_crossentropy",
  metrics = "accuracy"
)

history <- model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)
best_epoch <- which.min(history$metrics$val_loss)
best_loss <- history$metrics$val_loss[best_epoch] %>% round(3)
best_acc <- history$metrics$val_accuracy[best_epoch] %>% round(3)

glue("Our optimal loss is {best_loss} with an accuracy of {best_acc}")
Our optimal loss is 0.301 with an accuracy of 0.876
plot(history)

YOUR TURN! (5min)

Spend a few minutes adjusting this model and see how it impacts performance. You may want to test:

  • Does increasing and decreasing the word embedding dimension (output_dim) impacts performance?
  • How does the learning rate impact performance?
  • You may have noticed that we didn’t add any additional hidden layers to the densely connected portion of our model. Does adding 1 or 2 more hidden layers improve performance?
yourturn_model <- keras_model_sequential() %>%
  layer_embedding(
    input_dim = _____,  
    input_length = _____,   
    output_dim = _____           
    ) %>%  
  layer_flatten() %>% 
  layer_dense(units = ____, activation = ____) %>%
  layer_dense(units = 1, activation = "sigmoid")

yourturn_model %>% compile(
  optimizer = _____,
  loss = "binary_crossentropy",
  metrics = "accuracy"
)

yourturn_results <- yourturn_model %>% fit(
  x_train, y_train,
  epochs = 10,
  batch_size = 32,
  validation_split = 0.2
)

Comparing embeddings

Recall that the word embeddings we found for natural language modeling created results like:

# natural language modeling embeddings
get_similar_words("horrible", word_embeddings)
 horrible  terrible     awful       bad    acting 
1.0000000 0.9132471 0.8663343 0.8041792 0.7790165 

However, embeddings we find for classification tasks are not always so clean and intuitive. We can get the word embeddings from our classification model with:

wts <- get_weights(model)
embedding_wts <- wts[[1]]

The following just does some bookkeeping to extract the applicable words and assign them as row names to the embedding matrix.

words <- tokenizer$word_index %>% 
  as_tibble() %>% 
  pivot_longer(everything(), names_to = "word", values_to = "id") %>%
  filter(id <= tokenizer$num_words) %>%
  arrange(id)

row.names(embedding_wts) <- words$word

The following is one of the custom functions you imported from the helper_functions.R file. You can see the word embeddings that most closely align to a given word are not as intuitive as those produced from the natural language model. However, these are the embeddings that optimized for the classification procedure at hand.

similar_classification_words("horrible", embedding_wts)
 horrible      foul    source    rivals  homicide      fits 
1.0000000 0.7439281 0.7299364 0.7215308 0.7141167 0.7128572 

Here’s a handy sequence of code that uses the t-SNE methodology to visualize nearest neighbor word embeddings.

# plotting too many words makes the output hard to read
n_words_to_plot <- 1000

tsne <- Rtsne::Rtsne(
  X = embedding_wts[1:n_words_to_plot,], 
  perplexity = 100, 
  pca = FALSE
  )

p <- tsne$Y %>%
  as.data.frame() %>%
  mutate(word = row.names(embedding_wts)[1:n_words_to_plot]) %>%
  ggplot(aes(x = V1, y = V2, label = word)) + 
  geom_text(size = 3)

plotly::ggplotly(p)

Key takeaways

  • Word embeddings
    • Commonly used for language modeling and prediction tasks.
    • Provide a dense, relationship rich vector representation of words.
    • We can measure similarity with cosine similarity (other distance functions are also used).
    • See http://bit.ly/dl-06#12 for list of resources to go deeping into word embeddings.
  • Data prep
    • Use text_tokenizer() to define the text preprocessing we desire (defaults are good).
    • Use fit_text_tokenizer() to preprocess text (i.e. remove punctuation, standardize to lowercase).
    • Use texts_to_sequences() to convert standardized text to numeric representation of word index.
    • Use pad_sequences() to make all text sequences the same length. This length can be adjusted to improve performance.
  • Model training
    • Fit word embeddings with layer_embedding().
    • Tune output_dimension of the embeddings.
    • Apply layer_flatten() to convert embeddings to a 2D tensor and use layer_dense() for classification (there are alternatives which we will cover later).
LS0tCnRpdGxlOiAiTkxQOiBXb3JkIGVtYmVkZGluZ3MiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogdHJ1ZQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFKQpgYGAKCkluIHRoaXMgZXhhbXBsZSwgd2UgYXJlIGdvaW5nIHRvIGxlYXJuIGFib3V0IGFuIGFsdGVybmF0aXZlIG1ldGhvZCB0byBlbmNvZGUgCnRleHQgZGF0YSBrbm93biBhcyBfX193b3JkIGVtYmVkZGluZ3NfX18uIFRoaXMgaXMgYW4gaW5jb21wbGV0ZSB0dXRvcmlhbCBvbiAKd29yZCBlbWJlZGRpbmdzIGJ1dCB3aWxsIGF0IGxlYXN0IGdpdmUgeW91IHRoZSBiYXNpYyB1bmRlcnN0YW5kaW5nIG9uIHdoZW4sIHdoeSwKYW5kIGhvdyB3ZSB1c2UgdGhlbS4KCkxlYXJuaW5nIG9iamVjdGl2ZXM6CgotIFdoYXQgd29yZCBlbWJlZGRpbmdzIGFyZS4KLSBUaGUgdHdvIG1haW4gY29udGV4dHMgdGhhdCB3b3JkIGVtYmVkZGluZ3MgYXJlIHRyYWluZWQuCi0gV2hlbiB3ZSBzaG91bGQgdXNlIHdvcmQgZW1iZWRkaW5ncy4KLSBIb3cgdG8gdHJhaW4gd29yZCBlbWJlZGRpbmdzIGZvciBjbGFzc2lmaWNhdGlvbiBwdXJwb3Nlcy4KCiMgUmVxdWlyZW1lbnRzCgpgYGB7cn0KIyBJbml0aWFsaXplIHBhY2thZ2UKbGlicmFyeShrZXJhcykKbGlicmFyeShmcykKbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkoZ2x1ZSkKbGlicmFyeShwcm9ncmVzcykKCiMgaGVscGVyIGZ1bmN0aW9ucyB3ZSdsbCB1c2UgdG8gZXhwbG9yZSB3b3JkIGVtYmVkZGluZ3MKc291cmNlKCJoZWxwZXJfZnVuY3Rpb25zLlIiKQpgYGAKCiMgVGhlICJyZWFsIiBJTURCIGRhdGFzZXQKCktlcmFzIHByb3ZpZGVzIGEgYnVpbHQtaW4gSU1CRCBkYXRhc2V0IGBkYXRhc2V0X2ltZGIoKWAsIHdoaWNoIGNvbnRhaW5zIHRoZQp0ZXh0IG9mIDI1LDAwMCBtb3ZpZSByZXZpZXdzIHRoYXQgaGF2ZSBiZWVuIGNsYXNzaWZpZWQgYXMgbmV0IHBvc2l0aXZlIG9yIG5ldApuZWdhdGl2ZS4gSG93ZXZlciwgd2UgYXJlIGdvaW5nIHRvIHVzZSB0aGUgb3JpZ2luYWwgSU1EQiBtb3ZpZSByZXZpZXcgZmlsZXMgd2hpY2gKY2FuIGJlIGZvdW5kIGF0IGh0dHA6Ly9haS5zdGFuZm9yZC5lZHUvfmFtYWFzL2RhdGEvc2VudGltZW50L2FjbEltZGJfdjEudGFyLmd6LgpUaGlzIHRlbmRzIHRvIGhlbHAgc3R1ZGVudHMgYmV0dGVyIHVuZGVyc3RhbmQgdGhlIGVudGlyZSBkYXRhIHByZXAgcmVxdWlyZWQgZm9yCnRleHQuCgpZb3UgY2FuIGZpbmQgdGhlIGRvd25sb2FkIGluc3RydWN0aW9ucyBbaGVyZV0oaHR0cDovL2JpdC5seS9kbC1ycW10cykuIEZvciB0aG9zZQppbiB0aGUgd29ya3Nob3Agd2UgaGF2ZSBhbHJlYWR5IGRvd25sb2FkZWQgdGhpcyBkYXRhIGZvciB5b3UuCgpgYGB7cn0KaWYgKHN0cmluZ3I6OnN0cl9kZXRlY3QoaGVyZTo6aGVyZSgpLCAiY29uZi0yMDIwLXVzZXIiKSkgewogIGltZGJfZGlyIDwtICIvaG9tZS9jb25mLTIwMjAtdXNlci9kYXRhL2ltZGIiCn0gZWxzZSB7CiAgaW1kYl9kaXIgPC0gaGVyZTo6aGVyZSgibWF0ZXJpYWxzIiwgImRhdGEiLCAiaW1kYiIpCn0KZnM6OmRpcl90cmVlKGltZGJfZGlyLCB0eXBlID0gImRpcmVjdG9yeSIpCmBgYAoKWW91IGNhbiBzZWUgdGhlIGRhdGEgaGF2ZSBhbHJlYWR5IGJlZW4gc2VwYXJhdGVkIGludG8gdGVzdCB2cyB0cmFpbmluZyBzZXRzIGFuZCAKcG9zaXRpdmUgdnMgbmVnYXRpdmUgc2V0cy4gVGhlIGFjdHVhbCByZXZpZXdzIGFyZSBjb250YWluZWQgaW4gaW5kaXZpZHVhbCAudHh0CmZpbGVzLiBXZSBjYW4gdXNlIHRoaXMgc3RydWN0dXJlIHRvIG91ciBhZHZhbnRhZ2UgLSB0aGUgYmVsb3cgaXRlcmF0ZXMgb3ZlciBlYWNoCnJldmlldyBhbmQgCgoxLiBjcmVhdGVzIHRoZSBwYXRoIHRvIGVhY2ggaW5kaXZpZHVhbCByZXZpZXcgZmlsZSwKMi4gY3JlYXRzIGEgbGFiZWwgYmFzZWQgb24gdGhlICJuZWciIG9yICJwb3MiIGZvbGRlciB0aGUgcmV2aWV3IGlzIGluLAozLiBhbmQgc2F2ZXMgdGhlIG91dHB1dCBhcyBhIGRhdGEgZnJhbWUgd2l0aCBlYWNoIHJldmlldyBvbiBhbiBpbmRpdmlkdWFsIHJvdy4KCmBgYHtyfQp0cmFpbmluZ19maWxlcyA8LSBmaWxlLnBhdGgoaW1kYl9kaXIsICJ0cmFpbiIpICU+JQogIGRpcl9scyh0eXBlID0gImRpcmVjdG9yeSIpICU+JQogIG1hcChkaXJfbHMpICU+JQogIHNldF9uYW1lcyhiYXNlbmFtZSkgJT4lCiAgcGx5cjo6bGRwbHkoZGF0YV9mcmFtZSkgJT4lCiAgc2V0X25hbWVzKGMoImxhYmVsIiwgInBhdGgiKSkKCnRyYWluaW5nX2ZpbGVzCmBgYAoKV2UgY2FuIHNlZSBvdXIgcmVzcG9uc2Ugb2JzZXJ2YXRpb25zIGFyZSBiYWxhbmNlZDoKCmBgYHtyfQpjb3VudCh0cmFpbmluZ19maWxlcywgbGFiZWwpCmBgYAoKV2UgY2FuIG5vdyBpdGVyYXRlIG92ZXIgZWFjaCByb3cgYW5kCgoxLiBzYXZlIHRoZSBsYWJlbCBpbiBhIGBsYWJlbHNgIHZlY3RvciwKMi4gaW1wb3J0IHRoZSBtb3ZpZSByZXZpZXcgYW5kCjMuIHNhdmUgaW4gYSBgdGV4dHNgIHZlY3Rvci4KCmBgYHtyfQpvYnMgPC0gbnJvdyh0cmFpbmluZ19maWxlcykKbGFiZWxzIDwtIHZlY3Rvcihtb2RlID0gImludGVnZXIiLCBsZW5ndGggPSBvYnMpCnRleHRzIDwtIHZlY3Rvcihtb2RlID0gImNoYXJhY3RlciIsIGxlbmd0aCA9IG9icykKCiMgdGhpcyBqdXN0IGFsbG93cyB1cyB0byB0cmFjayBwcm9ncmVzcyBvZiBvdXIgbG9vcApwYiA8LSBwcm9ncmVzc19iYXIkbmV3KHRvdGFsID0gb2JzLCB3aWR0aCA9IDYwKQoKZm9yIChmaWxlIGluIHNlcV9sZW4ob2JzKSkgewogIHBiJHRpY2soKQogIAogIGxhYmVsIDwtIHRyYWluaW5nX2ZpbGVzW1tmaWxlLCAibGFiZWwiXV0KICBwYXRoIDwtIHRyYWluaW5nX2ZpbGVzW1tmaWxlLCAicGF0aCJdXQogIAogIGxhYmVsc1tmaWxlXSA8LSBpZmVsc2UobGFiZWwgPT0gIm5lZyIsIDAsIDEpCiAgdGV4dHNbZmlsZV0gPC0gcmVhZENoYXIocGF0aCwgbmNoYXJzID0gZmlsZS5zaXplKHBhdGgpKSAKICAKfQpgYGAKCldlIG5vdyBoYXZlIHR3byB2ZWN0b3JzLCBvbmUgY29uc2lzdGluZyBvZiB0aGUgbGFiZWxzLi4uCgpgYGB7cn0KdGFibGUobGFiZWxzKQpgYGAKCmFuZCB0aGUgb3RoZXIgaG9sZGluZyBlYWNoIHJldmlldy4KCmBgYHtyfQp0ZXh0c1sxXQpgYGAKCiMgRXhwbG9yYXRvcnkgdGV4dCBhbmFseXNpcwoKQSBsaXR0bGUgZXhwbG9yYXRvcnkgYW5hbHlzaXMgd2lsbCBzaG93IHVzIHRoZSB0b3RhbCBudW1iZXIgb2YgdW5pcXVlIHdvcmRzCmFjcm9zcyBvdXIgY29ycHVzIGFuZCB0aGUgYXZlcmFnZSBsZW5ndGggb2YgZWFjaCByZXZpZXcuIEl0cyBnb29kIHRvIGtub3cgdGhlCndvcmQgY291bnQgZGlzdHJpYnV0aW9uIG9mIHlvdXIgdGV4dCBhcyBsYXRlciBvbiB3ZSdsbCBtYWtlIGEgZGVjaXNpb24gb2YgaG93Cm1hbnkgd29yZHMgdG8ga2VlcC4KCmBgYHtyLCBmaWcuaGVpZ2h0PTMuNX0KdGV4dF9kZiA8LSB0ZXh0cyAlPiUKICB0aWJibGUoLm5hbWVfcmVwYWlyID0gfiAidGV4dCIpICU+JQogIG11dGF0ZSh0ZXh0X2xlbmd0aCA9IHN0cl9jb3VudCh0ZXh0LCAiXFx3KyIpKQoKdW5pcXVlX3dvcmRzIDwtIHRleHRfZGYgJT4lCiAgdGlkeXRleHQ6OnVubmVzdF90b2tlbnMod29yZCwgdGV4dCkgJT4lCiAgcHVsbCh3b3JkKSAlPiUKICBuX2Rpc3RpbmN0KCkKCmF2Z19yZXZpZXdfbGVuZ3RoIDwtIG1lZGlhbih0ZXh0X2RmJHRleHRfbGVuZ3RoLCBuYS5ybSA9IFRSVUUpCiAgCmdncGxvdCh0ZXh0X2RmLCBhZXModGV4dF9sZW5ndGgpKSArCiAgZ2VvbV9oaXN0b2dyYW0oYmlucyA9IDEwMCwgZmlsbCA9ICJncmV5NzAiLCBjb2xvciA9ICJncmV5NDAiKSArCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gYXZnX3Jldmlld19sZW5ndGgsIGNvbG9yID0gInJlZCIsIGx0eSA9ICJkYXNoZWQiKSArCiAgc2NhbGVfeF9sb2cxMCgiIyB3b3JkcyIpICsKICBnZ3RpdGxlKGdsdWUoIk1lZGlhbiByZXZpZXcgbGVuZ3RoIGlzIHthdmdfcmV2aWV3X2xlbmd0aH0gd29yZHMiKSwKICAgICAgICAgIHN1YnRpdGxlID0gZ2x1ZSgiVG90YWwgbnVtYmVyIG9mIHVuaXF1ZSB3b3JkcyBpcyB7dW5pcXVlX3dvcmRzfSIpKQpgYGAKCgojIFdvcmQgZW1iZWRkaW5ncyBmb3IgbGFuZ3VhZ2UgbW9kZWxpbmcKCldvcmQgZW1iZWRkaW5ncyBhcmUgZGVzaWduZWQgdG8gZW5jb2RlIGdlbmVyYWwgc2VtYW50aWMgcmVsYXRpb25zaGlwcyB3aGljaCBjYW4Kc2VydmUgdHdvIHByaW5jaXBsZSBwdXJwb3Nlcy4gVGhlIGZpcnN0IGlzIGZvciBfX19sYW5ndWFnZSBtb2RlbGluZ19fXyB3aGljaCAKYWltcyB0byBlbmNvZGUgd29yZHMgZm9yIHRoZSBwdXJwb3NlIG9mIHByZWRpY3Rpbmcgc3lub255bXMsIHNlbnRlbmNlIGNvbXBsZXRpb24sIAphbmQgd29yZCByZWxhdGlvbnNoaXBzLiBb4oS577iPXShodHRwOi8vYml0Lmx5L2RsLTA2KQoKQWx0aG91Z2ggd2UgYXJlIG5vdCBmb2N1c2luZyBvbiB3b3JkIGVtYmVkZGluZ3MgZm9yIHRoaXMgcHVycG9zZSwgSSBoYXZlIHdyaXR0ZW4KYSBjb3VwbGUgaGVscGVyIGZ1bmN0aW9ucyB0byB0cmFpbiB3b3JkIGVtYmVkZGluZ3MgZm9yIHRoaXMgcHVycG9zZS4gU2VlIHRoZQpjb2RlIGJlaGluZCB0aGVzZSBoZWxwZXIgZnVuY3Rpb25zIFtoZXJlXShodHRwczovL2JpdC5seS8zMkhDUDFHKS4KCmBgYHtyfQojIGNsZWFuIHVwIHRleHQgYW5kIGNvbXB1dGUgd29yZCBlbWJlZGRpbmdzCmNsZWFuX3RleHQgPC0gdG9sb3dlcih0ZXh0cykgJT4lCiAgc3RyX3JlcGxhY2VfYWxsKHBhdHRlcm4gPSAiW1s6cHVuY3Q6XSBdKyIsIHJlcGxhY2VtZW50ID0gIiAiKSAlPiUKICBzdHJfdHJpbSgpCgp3b3JkX2VtYmVkZGluZ3MgPC0gZ2V0X2VtYmVkZGluZ3MoY2xlYW5fdGV4dCkKYGBgCgpFeHBsb3JlIHlvdXIgb3duIHdvcmRzIQoKYGBge3J9CiMgZmluZCB3b3JkcyB3aXRoIHNpbWlsYXIgZW1iZWRkaW5ncwpnZXRfc2ltaWxhcl93b3JkcygiaG9ycmlibGUiLCB3b3JkX2VtYmVkZGluZ3MpCmBgYAoKCiMgV29yZCBlbWJlZGRpbmdzIGZvciBjbGFzc2lmaWNhdGlvbgoKVGhlIG90aGVyIHByaW5jaXBsZSBwdXJwb3NlIGZvciB3b3JkIGVtYmVkZGluZ3MgaXMgdG8gZW5jb2RlIHRleHQgZm9yIApjbGFzc2lmaWNhdGlvbiByZWFzb25zLiBJbiB0aGlzIGNhc2UsIHdlIHRyYWluIHRoZSB3b3JkIGVtYmVkZGluZ3MgdG8gdGFrZSBvbiAKd2VpZ2h0cyB0aGF0IG9wdGltaXplIHRoZSBjbGFzc2lmaWNhdGlvbiBsb3NzIGZ1bmN0aW9uLiBb4oS577iPXShodHRwOi8vYml0Lmx5L2RsLTA2IzEzKQoKIyMgUHJlcGFyZSBkYXRhCgpPdXIgcmVzcG9uc2UgdmFyaWFibGUgYGxhYmVsc2AgaXMgYWxyZWFkeSBhIHRlbnNvcjsgaG93ZXZlciwgd2Ugc3RpbGwgbmVlZCB0bwpwcmVwcm9jZXNzIG91ciB0ZXh0IGZlYXR1cmVzLiBUbyBkbyBzbyB3ZToKCjEuIFNwZWNpZnkgaG93IG1hbnkgd29yZHMgd2Ugd2FudCB0byBpbmNsdWRlLiBUaGlzIGV4YW1wbGUgdXNlcyB0aGUgMTAsMDAwIHdvcmRzCiAgIHdpdGggdGhlIGhpZ2hlc3QgdXNhZ2UgKGZyZXF1ZW5jeSkuCjIuIENyZWF0ZSBhIGB0ZXh0X3Rva2VuaXplcmAgb2JqZWN0IHdoaWNoIGRlZmluZXMgaG93IHdlIHdhbnQgdG8gcHJlcHJvY2VzcyB0aGUKICAgdGV4dCAoaS5lLiBjb252ZXJ0IHRvIGxvd2VyY2FzZSwgcmVtb3ZlIHB1bmN0dWF0aW9uLCB0b2tlbiBzcGxpdHRpbmcgCiAgIGNoYXJhY3RlcnMpLiBGb3IgdGhlIG1vc3QgcGFydCwgdGhlIGRlZmF1bHRzIGFyZSBzdWZmaWNpZW50LgozLiBBcHBseSB0aGUgdG9rZW5pemVyIHRvIG91ciB0ZXh0IHdpdGggYGZpdF90ZXh0X3Rva2VuaXplcmAuIFRoaXMgcmVzdWx0cyBpbiBhbgogICBvYmplY3Qgd2l0aCBtYW55IGRldGFpbHMgb2Ygb3VyIGNvcnB1cyAoaS5lLiB3b3JkIGNvdW50cywgd29yZCBpbmRleCkuCgpgYGB7cn0KdG9wX25fd29yZHMgPC0gMTAwMDAKCnRva2VuaXplciA8LSB0ZXh0X3Rva2VuaXplcihudW1fd29yZHMgPSB0b3Bfbl93b3JkcykgJT4lIAogIGZpdF90ZXh0X3Rva2VuaXplcih0ZXh0cykKCm5hbWVzKHRva2VuaXplcikKYGBgCgpXZSBoYXZlIG5vdyB0b2tlbml6ZWQgb3VyIHJldmlld3MuIFdlIGFyZSBjb25zaWRlcmluZyAxMCwwMDAgb2YgODgsNTgyIHRvdGFsCnVuaXF1ZSB3b3Jkcy4gVGhlIG1vc3QgY29tbW9uIHdvcmRzIGluY2x1ZGU6CiAgICAgCmBgYHtyfQpoZWFkKHRva2VuaXplciR3b3JkX2luZGV4KQpgYGAKCk5leHQsIHdlIGV4dHJhY3Qgb3VyIHZlY3Rvcml6ZWQgcmV2aWV3IGRhdGEgYXMgYSBsaXN0LiBFYWNoIHJldmlldyBpcyBlbmNvZGVkIGFzCmEgc2VxdWVuY2Ugb2Ygd29yZCBpbmRleGVzIChpbnRlZ2VycykuCgpgYGB7cn0Kc2VxdWVuY2VzIDwtIHRleHRzX3RvX3NlcXVlbmNlcyh0b2tlbml6ZXIsIHRleHRzKQoKIyBUaGUgdmVjdG9yaXplZCBmaXJzdCBpbnN0YW5jZToKc2VxdWVuY2VzW1sxXV0KYGBgCgpXZSBjYW4gbWFwIHRoZSBpbnRlZ2VyIHZhbHVlcyBiYWNrIHRvIHRoZSB3b3JkIGluZGV4LiBUaGUgaW50ZWdlciBudW1iZXIKY29ycmVzcG9uZHMgdG8gdGhlIHBvc2l0aW9uIGluIHRoZSB3b3JkIGNvdW50IGxpc3QgYW5kIHRoZSBuYW1lIG9mIHRoZSB2ZWN0b3IgaXMKdGhlIGFjdHVhbCB3b3JkLgoKYGBge3J9CnBhc3RlKHVubGlzdCh0b2tlbml6ZXIkaW5kZXhfd29yZClbc2VxdWVuY2VzW1sxXV1dICwgY29sbGFwc2UgPSAiICIpCmBgYAoKV2UgY2FuIHNlZSBob3cgb3VyIHRva2VuaXplciBjb252ZXJ0ZWQgb3VyIG9yaWdpbmFsIHRleHQgdG8gYSBjbGVhbmVkIHVwIAp2ZXJzaW9uOgoKYGBge3J9IApjYXQoY3JheW9uOjpibHVlKCJPcmlnaW5hbCB0ZXh0OlxuIikpCnRleHRzW1sxXV0KCmNhdChjcmF5b246OmJsdWUoIlxuUmV2aXNlZCB0ZXh0OlxuIikpCnBhc3RlKHVubGlzdCh0b2tlbml6ZXIkaW5kZXhfd29yZClbc2VxdWVuY2VzW1sxXV1dICwgY29sbGFwc2UgPSAiICIpCmBgYAoKTmV4dCwgc2luY2UgZWFjaCByZXZpZXcgaXMgYSBkaWZmZXJlbnQgbGVuZ3RoLCB3ZSBuZWVkIHRvIGxpbWl0IG91cnNlbHZlcyB0byBhCmNlcnRhaW4gbnVtYmVyIG9mIHdvcmRzIHNvIHRoYXQgYWxsIG91ciBmZWF0dXJlcyAocmV2aWV3cykgYXJlIHRoZSBzYW1lIGxlbmd0aC4KVGhpcyBzaG91bGQgYmUgdmlld2VkIGFzIGEgdHVuaW5nIHBhcmFtZXRlci4gCgpfX1RpcF9fOiBJIHR5cGljYWxseSBzdGFydCB3aXRoIHZhbHVlcyBhcm91bmQgdGhlIDUwJSAobWVkaWFuKSBidXQgdGhlbiBleHBsb3JlCnZhbHVlcyB0aGF0IHJlcHJlc2VudCAyNSUgJiA3NSUgcGVyY2VudGlsZSBvZiB0aGUgd29yZCBkaXN0cmlidXRpb24gd2hlbiB0dW5pbmcuCgpOb3RlIChgP3BhZF9zZXF1ZW5jZXNgKToKCiogQW55IHJldmlld3MgdGhhdCBhcmUgc2hvcnRlciB0aGFuIHRoaXMgbGVuZ3RoIHdpbGwgYmUgcGFkZGVkLgoqIEFueSByZXZpZXdzIHRoYXQgYXJlIGxvbmdlciB0aGFuIHRoaXMgbGVuZ3RoIHdpbGwgYmUgdHJ1bmNhdGVkLgoKYGBge3J9Cm1heF9sZW4gPC0gMTUwCmZlYXR1cmVzIDwtIHBhZF9zZXF1ZW5jZXMoc2VxdWVuY2VzLCBtYXhsZW4gPSBtYXhfbGVuKQpgYGAKClNpbmNlIHRoaXMgcmV2aWV3IGluY2x1ZGVzIGxlc3MgdGhhbiAxNTAgd29yZHMgZnJvbSBvdXIgd29yZCBpbmRleCBvZiAxMEsgbW9zdApmcmVxdWVudCB3b3JkcywgaXQgcGFkcyB0aGUgZnJvbnQtZW5kIHdpdGggemVyb3MgKHNlZSBgP3BhZF9zZXF1ZW5jZXMoKWAgZm9yCmFsdGVybmF0aXZlIHBhZGRpbmcgb3B0aW9ucykuCgpgYGB7cn0KZmVhdHVyZXNbMSxdCmBgYAoKU28sIGluIGVzc2VuY2UsIHdlIGhhdmUgY3JlYXRlZCBhbiBpbnB1dCB0aGF0IGlzIGEgbnVtZXJpYyByZXByZXNlbnRhdGlvbiBvZgp0aGlzOgoKYGBge3J9CmZlYXR1cmVzWzEsXSAlPiUgCiAgbWFwX2Nocih+IGlmZWxzZSgueCA9PSAwLCAiPHBhZD4iLCB1bmxpc3QodG9rZW5pemVyJGluZGV4X3dvcmRbLnhdKSkpICU+JQogIGNhdCgpCmBgYAoKIyMjIFlvdXIgVHVybiEKCkNoZWNrIG91dCBkaWZmZXJlbnQgcmV2aWV3cyBhbmQgc2VlIGhvdyB3ZSBoYXZlIHRyYW5zZm9ybWVkIHRoZSBkYXRhLiBSZW1vdmUgCmBldmFsPUZBTFNFYCB0byBydW4uCgpgYGB7ciwgZXZhbD1GQUxTRX0KIyB1c2UgcmV2aWV3IG51bWJlciAoaS5lLiAyLCAxMCwgMTUwKQp3aGljaF9yZXZpZXcgPC0gX19fXwogIApjYXQoY3JheW9uOjpibHVlKCJPcmlnaW5hbCB0ZXh0OlxuIikpCnRleHRzW1t3aGljaF9yZXZpZXcgXV0KCmNhdChjcmF5b246OmJsdWUoIlxuUmV2aXNlZCB0ZXh0OlxuIikpCnBhc3RlKHVubGlzdCh0b2tlbml6ZXIkaW5kZXhfd29yZClbZmVhdHVyZXNbd2hpY2hfcmV2aWV3ICxdXSAsIGNvbGxhcHNlID0gIiAiKQoKY2F0KGNyYXlvbjo6Ymx1ZSgiXG5FbmNvZGVkIHRleHQ6XG4iKSkKZmVhdHVyZXNbd2hpY2hfcmV2aWV3LF0gJT4lIAogIG1hcF9jaHIofiBpZmVsc2UoLnggPT0gMCwgIjxwYWQ+IiwgdW5saXN0KHRva2VuaXplciRpbmRleF93b3JkWy54XSkpKSAlPiUKICBjYXQoKQpgYGAKCgpPdXIgZGF0YSBpcyBub3cgcHJlcHJvY2Vzc2VkISBXZSBoYXZlIGByIG5yb3coZmVhdHVyZXMpYCBvYnNlcnZhdGlvbnMgYW5kIApgciBuY29sKGZlYXR1cmVzKWAgZmVhdHVyZXMuIE91ciBgZmVhdHVyZXNgIGRhdGEgaXMgYSBtYXRyaXggd2hlcmUgZWFjaCByb3cgaXMKYSBzaW5nbGUgb2JzZXJ2YXRpb24gYW5kIGVhY2ggY29sdW1uIHJlcHJlc2VudHMgdGhlIHdvcmRzIGluIHRoZSByZXZpZXcgaW4gdGhlCm9yZGVyIHRoYXQgdGhleSBhcHBlYXIuCgpgYGB7cn0KZGltKGZlYXR1cmVzKQpsZW5ndGgobGFiZWxzKQpgYGAKCgojIyBNb2RlbCB0cmFpbmluZwoKVG8gdHJhaW4gb3VyIG1vZGVsIHdlIHdpbGwgdXNlIHRoZSBgdmFsaWRhdGlvbl9zcGxpdGAgcHJvY2VkdXJlIHdpdGhpbiBgZml0YC4gClJlbWVtYmVyLCB0aGlzIHRha2VzIHRoZSBsYXN0IFhYJSBvZiBvdXIgZGF0YSB0byBiZSB1c2VkIGFzIG91ciB2YWxpZGF0aW9uIHNldC4gCkJ1dCBpZiB5b3UgcmVjYWxsLCBvdXIgZGF0YSB3YXMgb3JnYW5pemVkIGluIG5lZyBhbmQgcG9zIGZvbGRlcnMgc28gd2Ugc2hvdWxkIApyYW5kb21pemUgb3VyIGRhdGEgdG8gbWFrZSBzdXJlIG91ciB2YWxpZGF0aW9uIHNldCBkb2Vzbid0IGVuZCB1cCBiZWluZyBhbGwgCnBvc2l0aXZlIG9yIG5lZ2F0aXZlIHJldmlld3MhCgpgYGB7cn0Kc2V0LnNlZWQoMTIzKQppbmRleCA8LSBzYW1wbGUoMTpucm93KGZlYXR1cmVzKSkKCnhfdHJhaW4gPC0gZmVhdHVyZXNbaW5kZXgsIF0KeV90cmFpbiA8LSBsYWJlbHNbaW5kZXhdCmBgYAoKVG8gY3JlYXRlIG91ciBuZXR3b3JrIGFyY2hpdGVjdHVyZSB0aGF0IGluY2x1ZGVzIHdvcmQgZW1iZWRkaW5ncywgd2UgbmVlZCB0byAKaW5jbHVkZSB0d28gdGhpbmdzOgoKMS4gYGxheWVyX2VtYmVkZGluZ2AgbGF5ZXIgdGhhdCBjcmVhdGVzIHRoZSBlbWJlZGRpbmdzLAoyLiBgbGF5ZXJfZmxhdHRlbmAgdG8gZmxhdHRlbiBvdXIgZW1iZWRkaW5ncyB0byBhIDJEIHRlbnNvciBmb3Igb3VyIGRlbnNlbHkgCiAgICBjb25uZWN0ZWQgcG9ydGlvbiBvZiBvdXIgbW9kZWwKICAgIApfX05vdGVfXzoKCiogYGlucHV0X2RpbWAgJiBgaW5wdXRfbGVuZ3RoYCBhcmUgY29uc2lkZXJlZCBwcmUtcHJvY2Vzc2luZyBoeXBlcnBhcmFtZXRlcnMuCiogYG91dHB1dF9kaW1gIGlzIG91ciB3b3JkIGVtYmVkZGluZ3MgaHlwZXJwYXJhbWV0ZXIuCiogVGhleSBhbGwgaGF2ZSBpbnRlcmFjdGlvbiBlZmZlY3RzCgpgYGB7cn0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IHRvcF9uX3dvcmRzLCAgICMgbnVtYmVyIG9mIHdvcmRzIHdlIGFyZSBjb25zaWRlcmluZwogICAgaW5wdXRfbGVuZ3RoID0gbWF4X2xlbiwgICAgIyBsZW5ndGggdGhhdCB3ZSBoYXZlIHNldCBlYWNoIHJldmlldyB0bwogICAgb3V0cHV0X2RpbSA9IDMyICAgICAgICAgICAgIyBsZW5ndGggb2Ygb3VyIHdvcmQgZW1iZWRkaW5ncwogICAgKSAlPiUgIAogIGxheWVyX2ZsYXR0ZW4oKSAlPiUgCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiKQoKc3VtbWFyeShtb2RlbCkKYGBgCgpUaGUgcmVzdCBvZiBvdXIgbW9kZWxpbmcgcHJvY2VkdXJlIGZvbGxvd3MgdGhlIHNhbWUgcHJvdG9jb2xzIHRoYXQgeW91J3ZlIHNlZW4gCmluIHRoZSBvdGhlciBtb2R1bGVzLgoKYGBge3J9Cm1vZGVsICU+JSBjb21waWxlKAogIG9wdGltaXplciA9ICJybXNwcm9wIiwKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLAogIG1ldHJpY3MgPSAiYWNjdXJhY3kiCikKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLCB5X3RyYWluLAogIGVwb2NocyA9IDEwLAogIGJhdGNoX3NpemUgPSAzMiwKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yCikKYGBgCgpgYGB7cn0KYmVzdF9lcG9jaCA8LSB3aGljaC5taW4oaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKQpiZXN0X2xvc3MgPC0gaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzW2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQpiZXN0X2FjYyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2FjY3VyYWN5W2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQoKZ2x1ZSgiT3VyIG9wdGltYWwgbG9zcyBpcyB7YmVzdF9sb3NzfSB3aXRoIGFuIGFjY3VyYWN5IG9mIHtiZXN0X2FjY30iKQpgYGAKCgpgYGB7ciwgbWVzc2FnZT1GQUxTRX0KcGxvdChoaXN0b3J5KQpgYGAKCiMjIFlPVVIgVFVSTiEgKDVtaW4pCgpTcGVuZCBhIGZldyBtaW51dGVzIGFkanVzdGluZyB0aGlzIG1vZGVsIGFuZCBzZWUgaG93IGl0IGltcGFjdHMgcGVyZm9ybWFuY2UuIFlvdQptYXkgd2FudCB0byB0ZXN0OgoKLSBEb2VzIGluY3JlYXNpbmcgYW5kIGRlY3JlYXNpbmcgdGhlIHdvcmQgZW1iZWRkaW5nIGRpbWVuc2lvbiAoYG91dHB1dF9kaW1gKQogIGltcGFjdHMgcGVyZm9ybWFuY2U/Ci0gSG93IGRvZXMgdGhlIGxlYXJuaW5nIHJhdGUgaW1wYWN0IHBlcmZvcm1hbmNlPwotIFlvdSBtYXkgaGF2ZSBub3RpY2VkIHRoYXQgd2UgZGlkbid0IGFkZCBhbnkgYWRkaXRpb25hbCBoaWRkZW4gbGF5ZXJzIHRvIHRoZQogIGRlbnNlbHkgY29ubmVjdGVkIHBvcnRpb24gb2Ygb3VyIG1vZGVsLiAgRG9lcyBhZGRpbmcgMSBvciAyIG1vcmUgaGlkZGVuIGxheWVycwogIGltcHJvdmUgcGVyZm9ybWFuY2U/CgpgYGB7ciwgZXZhbD1GQUxTRX0KeW91cnR1cm5fbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IF9fX19fLCAgCiAgICBpbnB1dF9sZW5ndGggPSBfX19fXywgICAKICAgIG91dHB1dF9kaW0gPSBfX19fXyAgICAgICAgICAgCiAgICApICU+JSAgCiAgbGF5ZXJfZmxhdHRlbigpICU+JSAKICBsYXllcl9kZW5zZSh1bml0cyA9IF9fX18sIGFjdGl2YXRpb24gPSBfX19fKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEsIGFjdGl2YXRpb24gPSAic2lnbW9pZCIpCgp5b3VydHVybl9tb2RlbCAlPiUgY29tcGlsZSgKICBvcHRpbWl6ZXIgPSBfX19fXywKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLAogIG1ldHJpY3MgPSAiYWNjdXJhY3kiCikKCnlvdXJ0dXJuX3Jlc3VsdHMgPC0geW91cnR1cm5fbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLCB5X3RyYWluLAogIGVwb2NocyA9IDEwLAogIGJhdGNoX3NpemUgPSAzMiwKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yCikKYGBgCgojIENvbXBhcmluZyBlbWJlZGRpbmdzCgpSZWNhbGwgdGhhdCB0aGUgd29yZCBlbWJlZGRpbmdzIHdlIGZvdW5kIGZvciBuYXR1cmFsIGxhbmd1YWdlIG1vZGVsaW5nIGNyZWF0ZWQgCnJlc3VsdHMgbGlrZToKCmBgYHtyfQojIG5hdHVyYWwgbGFuZ3VhZ2UgbW9kZWxpbmcgZW1iZWRkaW5ncwpnZXRfc2ltaWxhcl93b3JkcygiaG9ycmlibGUiLCB3b3JkX2VtYmVkZGluZ3MpCmBgYAoKSG93ZXZlciwgZW1iZWRkaW5ncyB3ZSBmaW5kIGZvciBjbGFzc2lmaWNhdGlvbiB0YXNrcyBhcmUgbm90IGFsd2F5cyBzbyBjbGVhbiBhbmQgCmludHVpdGl2ZS4gV2UgY2FuIGdldCB0aGUgd29yZCBlbWJlZGRpbmdzIGZyb20gb3VyIGNsYXNzaWZpY2F0aW9uIG1vZGVsIHdpdGg6CgpgYGB7cn0Kd3RzIDwtIGdldF93ZWlnaHRzKG1vZGVsKSAgIyByZXR1cm5zIGEgbGlzdCB3aXRoIGVhY2ggbGF5ZXJzIHdlaWdodHMKZW1iZWRkaW5nX3d0cyA8LSB3dHNbWzFdXSAgIyBnZXQgdGhlIGZpcnN0IGxheWVyJ3Mgd2VpZ2h0cwpgYGAKClRoZSBmb2xsb3dpbmcganVzdCBkb2VzIHNvbWUgYm9va2tlZXBpbmcgdG8gZXh0cmFjdCB0aGUgYXBwbGljYWJsZSB3b3JkcyBhbmQgCmFzc2lnbiB0aGVtIGFzIHJvdyBuYW1lcyB0byB0aGUgZW1iZWRkaW5nIG1hdHJpeC4KCmBgYHtyfQp3b3JkcyA8LSB0b2tlbml6ZXIkd29yZF9pbmRleCAlPiUgCiAgYXNfdGliYmxlKCkgJT4lIAogIHBpdm90X2xvbmdlcihldmVyeXRoaW5nKCksIG5hbWVzX3RvID0gIndvcmQiLCB2YWx1ZXNfdG8gPSAiaWQiKSAlPiUKICBmaWx0ZXIoaWQgPD0gdG9rZW5pemVyJG51bV93b3JkcykgJT4lCiAgYXJyYW5nZShpZCkKCnJvdy5uYW1lcyhlbWJlZGRpbmdfd3RzKSA8LSB3b3JkcyR3b3JkCmBgYAoKVGhlIGZvbGxvd2luZyBpcyBvbmUgb2YgdGhlIGN1c3RvbSBmdW5jdGlvbnMgeW91IGltcG9ydGVkIGZyb20gdGhlIApbaGVscGVyX2Z1bmN0aW9ucy5SXShodHRwczovL2JpdC5seS8zMkhDUDFHKSBmaWxlLiBZb3UgY2FuIHNlZSB0aGUgd29yZCAKZW1iZWRkaW5ncyB0aGF0IG1vc3QgY2xvc2VseSBhbGlnbiB0byBhIGdpdmVuIHdvcmQgYXJlIG5vdCBhcyBpbnR1aXRpdmUgYXMgdGhvc2UKcHJvZHVjZWQgZnJvbSB0aGUgbmF0dXJhbCBsYW5ndWFnZSBtb2RlbC4gSG93ZXZlciwgdGhlc2UgYXJlIHRoZSBlbWJlZGRpbmdzIHRoYXQKb3B0aW1pemVkIGZvciB0aGUgY2xhc3NpZmljYXRpb24gcHJvY2VkdXJlIGF0IGhhbmQuCgpgYGB7cn0Kc2ltaWxhcl9jbGFzc2lmaWNhdGlvbl93b3JkcygiaG9ycmlibGUiLCBlbWJlZGRpbmdfd3RzKQpgYGAKIApIZXJlJ3MgYSBoYW5keSBzZXF1ZW5jZSBvZiBjb2RlIHRoYXQgdXNlcyB0aGUgW3QtU05FXShodHRwczovL2JpdC5seS8yckRrNnJzKSAKbWV0aG9kb2xvZ3kgdG8gdmlzdWFsaXplIG5lYXJlc3QgbmVpZ2hib3Igd29yZCBlbWJlZGRpbmdzLgoKYGBge3IsIGZpZy53aWR0aD0xMCwgZmlnLmhlaWdodD02fQojIHBsb3R0aW5nIHRvbyBtYW55IHdvcmRzIG1ha2VzIHRoZSBvdXRwdXQgaGFyZCB0byByZWFkCm5fd29yZHNfdG9fcGxvdCA8LSAxMDAwCgp0c25lIDwtIFJ0c25lOjpSdHNuZSgKICBYID0gZW1iZWRkaW5nX3d0c1sxOm5fd29yZHNfdG9fcGxvdCxdLCAKICBwZXJwbGV4aXR5ID0gMTAwLCAKICBwY2EgPSBGQUxTRQogICkKCnAgPC0gdHNuZSRZICU+JQogIGFzLmRhdGEuZnJhbWUoKSAlPiUKICBtdXRhdGUod29yZCA9IHJvdy5uYW1lcyhlbWJlZGRpbmdfd3RzKVsxOm5fd29yZHNfdG9fcGxvdF0pICU+JQogIGdncGxvdChhZXMoeCA9IFYxLCB5ID0gVjIsIGxhYmVsID0gd29yZCkpICsgCiAgZ2VvbV90ZXh0KHNpemUgPSAzKQoKcGxvdGx5OjpnZ3Bsb3RseShwKQpgYGAKIAojIEtleSB0YWtlYXdheXMKIAoqIFdvcmQgZW1iZWRkaW5ncwogICAtIENvbW1vbmx5IHVzZWQgZm9yIGxhbmd1YWdlIG1vZGVsaW5nIGFuZCBwcmVkaWN0aW9uIHRhc2tzLgogICAtIFByb3ZpZGUgYSBkZW5zZSwgcmVsYXRpb25zaGlwIHJpY2ggdmVjdG9yIHJlcHJlc2VudGF0aW9uIG9mIHdvcmRzLgogICAtIFdlIGNhbiBtZWFzdXJlIHNpbWlsYXJpdHkgd2l0aCBjb3NpbmUgc2ltaWxhcml0eSAob3RoZXIgZGlzdGFuY2UgZnVuY3Rpb25zCiAgICAgIGFyZSBhbHNvIHVzZWQpLgogICAtIFNlZSBodHRwOi8vYml0Lmx5L2RsLTA2IzEyIGZvciBsaXN0IG9mIHJlc291cmNlcyB0byBnbyBkZWVwaW5nIGludG8gd29yZAogICAgIGVtYmVkZGluZ3MuCiogRGF0YSBwcmVwCiAgIC0gVXNlIGB0ZXh0X3Rva2VuaXplcigpYCB0byBkZWZpbmUgdGhlIHRleHQgcHJlcHJvY2Vzc2luZyB3ZSBkZXNpcmUgKGRlZmF1bHRzCiAgICAgYXJlIGdvb2QpLgogICAtIFVzZSBgZml0X3RleHRfdG9rZW5pemVyKClgIHRvIHByZXByb2Nlc3MgdGV4dCAoaS5lLiByZW1vdmUgcHVuY3R1YXRpb24sCiAgICAgc3RhbmRhcmRpemUgdG8gbG93ZXJjYXNlKS4KICAgLSBVc2UgYHRleHRzX3RvX3NlcXVlbmNlcygpYCB0byBjb252ZXJ0IHN0YW5kYXJkaXplZCB0ZXh0IHRvIG51bWVyaWMKICAgICByZXByZXNlbnRhdGlvbiBvZiB3b3JkIGluZGV4LgogICAtIFVzZSBgcGFkX3NlcXVlbmNlcygpYCB0byBtYWtlIGFsbCB0ZXh0IHNlcXVlbmNlcyB0aGUgc2FtZSBsZW5ndGguIFRoaXMKICAgICBsZW5ndGggY2FuIGJlIGFkanVzdGVkIHRvIGltcHJvdmUgcGVyZm9ybWFuY2UuCiogTW9kZWwgdHJhaW5pbmcKICAgLSBGaXQgd29yZCBlbWJlZGRpbmdzIHdpdGggYGxheWVyX2VtYmVkZGluZygpYC4KICAgLSBUdW5lIGBvdXRwdXRfZGltYGVuc2lvbiBvZiB0aGUgZW1iZWRkaW5ncy4KICAgLSBBcHBseSBgbGF5ZXJfZmxhdHRlbigpYCB0byBjb252ZXJ0IGVtYmVkZGluZ3MgdG8gYSAyRCB0ZW5zb3IgYW5kIHVzZQogICAgIGBsYXllcl9kZW5zZSgpYCBmb3IgY2xhc3NpZmljYXRpb24gKHRoZXJlIGFyZSBhbHRlcm5hdGl2ZXMgd2hpY2ggd2Ugd2lsbAogICAgIGNvdmVyIGxhdGVyKS4=