Report: N-Gram Model Development and Prediction Plan

Project Overview

The goal remains to build a next-word prediction model using a 600 MB English corpus in en_nlp.db, and deploy it via a Shiny app. We’ve completed a trigram model and now aim to explore its statistical properties to optimize the prediction algorithm.

4 SQLite databases were created. One for each language: english - en_nlp.dbetc.

-- files table origanly had language but for size database was split into langages
CREATE TABLE files (
      file_id INTEGER PRIMARY KEY AUTOINCREMENT,
      file_name TEXT, -- full file path 
      file_size_mb REAL
    )
--  text loaded into sentences tables
CREATE TABLE sentences (
      sentence_id INTEGER PRIMARY KEY AUTOINCREMENT,
      file_id INTEGER,
      position INTEGER,
      text_content TEXT, 
      word_count INTEGER,
      FOREIGN KEY (file_id) REFERENCES files(file_id)
    )
-- ngrams table
CREATE TABLE ngrams (
    prefix TEXT, 
    next_word TEXT, 
    count INTEGER, 
    PRIMARY KEY (prefix, next_word)
    )

Exploratory Analysis Plan

To refine the model, we’ll analyze word and n-gram frequencies, dictionary coverage, foreign language influence, and coverage enhancement strategies. Below are the new questions, approaches, and proposed outputs.

Distributions of word frequencies

library(RSQLite)
library(dplyr)
library(tokenizers)
library(ggplot2)
library(hunspell)  # For foreign word detection
library(progress)

# Connect to database
conn <- dbConnect(SQLite(), "en_nlp.db")

sample_sentences <- function(conn, sample_size) {
    query <- "
        SELECT CAST(text_content AS TEXT) AS sentence 
        FROM sentences 
        WHERE sentence_id IN (
            SELECT sentence_id 
            FROM sentences 
            ORDER BY RANDOM() 
            LIMIT ?
        )"
    data <- dbGetQuery(conn, query, params = list(sample_size))
    data$sentence <- as.character(data$sentence)
    data$sentence[is.na(data$sentence)] <- ""
    data
}

# Sample 100,000 sentences
sample_size <- 100000
sampled_sentences <- sample_sentences(conn, sample_size)
cat("Sampled", nrow(sampled_sentences), "sentences\n")
Sampled 100000 sentences
# Tokenize all sentences into words
words_list <- lapply(sampled_sentences$sentence, tokenize_words, lowercase = TRUE, strip_punct = TRUE)
words <- unlist(words_list)

# Count word frequencies
word_freq <- table(words) %>% as.data.frame() %>% 
    rename(word = words, freq = Freq) %>% 
    arrange(desc(freq)) %>% 
    mutate(rank = row_number())

# Plot: Log-Log Word Frequency vs. Rank (Zipf’s Law)
p1 <- ggplot(word_freq, aes(x = rank, y = freq)) +
    geom_line() +
    scale_x_log10("Rank (log scale)") +
    scale_y_log10("Frequency (log scale)") +
    ggtitle("Word Frequency Distribution (Zipf’s Law)") +
    theme_minimal()
print(p1)

#ggsave("word_freq_zipf.png", p1, width = 8, height = 6)

# Table: Top 10 Frequent Words
top_words <- head(word_freq, 10) %>% select(word, freq)
print(top_words)
#write.csv(top_words, "top_words.csv", row.names = FALSE)

Frequencies of 2-Grams and 3-Grams

# Functions for bigram and trigram extraction
extract_bigrams <- function(sentence) {
    words <- tokenize_words(sentence, lowercase = TRUE, strip_punct = TRUE)[[1]]
    if (length(words) < 2) return(data.frame(prefix = character(), next_word = character()))
    data.frame(
        prefix = words[1:(length(words)-1)],
        next_word = words[2:length(words)]
    )
}

extract_trigrams <- function(sentence) {
    words <- tokenize_words(sentence, lowercase = TRUE, strip_punct = TRUE)[[1]]
    if (length(words) < 3) return(data.frame(prefix = character(), next_word = character()))
    n <- length(words) - 2
    data.frame(
        prefix = sapply(1:n, function(i) paste(words[i], words[i+1], sep=" ")),
        next_word = words[(1:n) + 2]
    )
}

# Generate bigrams and trigrams with progress bar
pb <- progress_bar$new(total = nrow(sampled_sentences), format = "[:bar] :percent eta: :eta")
bigrams_list <- list()
trigrams_list <- list()
for (i in 1:nrow(sampled_sentences)) {
    #pb$tick()
    sentence <- sampled_sentences$sentence[i]
    bigrams_list[[i]] <- extract_bigrams(sentence)
    trigrams_list[[i]] <- extract_trigrams(sentence)
}

# Combine and count
bigrams <- bind_rows(bigrams_list) %>% 
    group_by(prefix, next_word) %>% 
    summarise(freq = n(), .groups = "drop") %>% 
    arrange(desc(freq))
trigrams <- bind_rows(trigrams_list) %>% 
    group_by(prefix, next_word) %>% 
    summarise(freq = n(), .groups = "drop") %>% 
    arrange(desc(freq))

# Plot: Top 20 Bigrams and Trigrams Bar Plot
top_bigrams <- head(bigrams, 20) %>% mutate(n_gram = paste(prefix, next_word))
top_trigrams <- head(trigrams, 20) %>% mutate(n_gram = paste(prefix, next_word))
p2 <- ggplot(bind_rows(mutate(top_bigrams, type = "Bigram"), mutate(top_trigrams, type = "Trigram")), 
             aes(x = reorder(n_gram, freq), y = freq, fill = type)) +
    geom_bar(stat = "identity") +
    coord_flip() +
    labs(x = "N-Gram", y = "Frequency", title = "Top 20 Bigrams and Trigrams") +
    facet_wrap(~type, scales = "free_y") +
    theme_minimal()
print(p2)

#ggsave("ngram_freq_bar.png", p2, width = 10, height = 6)

# Table: N-Gram Frequency Summary
ngram_summary <- data.frame(
    "N-Gram Type" = c("Bigram", "Trigram"),
    "Unique Count" = c(nrow(bigrams), nrow(trigrams)),
    "Avg Frequency" = c(mean(bigrams$freq), mean(trigrams$freq)),
    "Max Frequency" = c(max(bigrams$freq), max(trigrams$freq))
)
print("N-Gram Frequency Summary:")
[1] "N-Gram Frequency Summary:"
print(ngram_summary)
#write.csv(ngram_summary, "ngram_summary.csv", row.names = FALSE)

Dictionary Coverage (50% and 90%)

# Calculate cumulative coverage
total_instances <- sum(word_freq$freq)
word_freq <- word_freq %>% 
    mutate(cum_freq = cumsum(freq), 
           cum_percent = cum_freq / total_instances * 100)
cutoff_50 <- word_freq %>% filter(cum_percent >= 50) %>% slice(1)
cutoff_90 <- word_freq %>% filter(cum_percent >= 90) %>% slice(1)

# Plot: Cumulative Frequency Curve
p3 <- ggplot(word_freq, aes(x = rank, y = cum_percent)) +
    geom_line() +
    geom_vline(xintercept = cutoff_50$rank, linetype = "dashed", color = "blue") +
    geom_vline(xintercept = cutoff_90$rank, linetype = "dashed", color = "red") +
    labs(x = "Number of Unique Words", y = "Cumulative % of Instances", 
         title = "Dictionary Coverage") +
    annotate("text", x = cutoff_50$rank, y = 60, label = "50%", color = "blue") +
    annotate("text", x = cutoff_90$rank, y = 95, label = "90%", color = "red") +
    theme_minimal()
print(p3)

#ggsave("coverage_curve.png", p3, width = 8, height = 6)

# Table: Coverage Thresholds
coverage_table <- data.frame(
    Coverage = c("50%", "90%"),
    "Unique Words" = c(cutoff_50$rank, cutoff_90$rank),
    "Example Words" = c(cutoff_50$word, cutoff_90$word)
)

Foreign Language Words

Pruning words with less than 5 frequency seem to improve “foreign” words detection (expected 5%-10%). However “foreign” is misleading as the top foreign words are misspelled English words. I used SCOWL to match English words. Tokenizing words into stems proved beneficial.

# Tokenize and clean with regex
words_list <- lapply(sampled_sentences$sentence, tokenize_word_stems, language = "english")
words <- unlist(words_list)
words_clean <- gsub("[[:punct:]]|[0-9]", "", words)

word_freq <- table(words_clean) %>% as.data.frame() %>% 
    rename(word = words_clean, freq = Freq) %>% 
    arrange(desc(freq)) %>% 
    mutate(rank = row_number())

# Load SCOWL English word list
english_words <- readLines("scowl_words.txt", encoding = "UTF-8", warn = FALSE)
# Add minimal extras (slang, abbreviations)
extra_words <- c("dont", "wont", "cant", "gonna", "yall", "aint", "im", "ive", "id", "youre", 
                 "its", "theyre", "wasnt", "isnt", "didnt", "thats", "heres", "theres",
                 "blog", "rt", "lol", "haha", "dr")
english_words_clean <- c(english_words, extra_words)

# Classify words
word_freq$word <- as.character(word_freq$word)
word_freq$is_english <- word_freq$word %in% english_words_clean

foreign_words <- word_freq %>% filter(!is_english)

# Proportion check (unfiltered)
english_count <- sum(word_freq$freq[word_freq$is_english])
foreign_count <- sum(word_freq$freq[!word_freq$is_english])
cat("English instances:", english_count, "\n")
English instances: 1306877 
cat("Foreign instances:", foreign_count, "\n")
Foreign instances: 245355 
cat("Foreign proportion:", foreign_count / (english_count + foreign_count) * 100, "%\n")
Foreign proportion: 15.80659 %
# Filter high-frequency "foreign" words
foreign_words_filtered <- foreign_words %>% filter(freq < 5)

# Proportion check (filtered)
english_count_filtered <- english_count + sum(foreign_words$freq[foreign_words$freq >= 5])
foreign_count_filtered <- sum(foreign_words_filtered$freq)
cat("English instances (pruned):", english_count_filtered, "\n")
English instances (pruned): 1516671 
cat("Foreign instances (pruned):", foreign_count_filtered, "\n")
Foreign instances (pruned): 35561 
cat("Foreign proportion (pruned):", foreign_count_filtered / (english_count_filtered + foreign_count_filtered) * 100, "%\n")
Foreign proportion (pruned): 2.290959 %
# Plot: Pie Chart (filtered)
pie_data <- data.frame(category = c("English", "Foreign"), 
                       count = c(english_count_filtered, foreign_count_filtered))
p4 <- ggplot(pie_data, aes(x = "", y = count, fill = category)) +
    geom_bar(stat = "identity", width = 1) +
    coord_polar("y") +
    labs(title = "English vs. Foreign Word Instances (Filtered)") +
    theme_void() +
    scale_fill_manual(values = c("English" = "blue", "Foreign" = "red"))
print(p4)



# Table: Top 10 Suspected Foreign Words
top_foreign <- head(foreign_words_filtered, 10) %>% select(word, freq)
print("Top 10 Suspected Foreign Words:")
[1] "Top 10 Suspected Foreign Words:"
print(top_foreign)
NA
NA
NA

Increasing Coverage

Using for rare words (freq < 5), reducing vocabulary size while maintaining phrase coverage.

# Simulate clustering (simplified: assume rare words grouped as <UNK>)
vocab_full <- word_freq %>%
    group_by(word) %>% 
    summarise(freq = sum(freq)) %>% 
    arrange(desc(freq)) %>% 
    mutate(cum_freq = cumsum(freq), cum_percent = cum_freq / total_instances * 100)

vocab_reduced <- word_freq %>% 
    mutate(word = ifelse(freq < 5, "<UNK>", word)) %>% 
    group_by(word) %>% 
    summarise(freq = sum(freq)) %>% 
    arrange(desc(freq)) %>% 
    mutate(cum_freq = cumsum(freq), cum_percent = cum_freq / total_instances * 100)

# Plot: Coverage vs. Dictionary Size
p5 <- ggplot() +
    geom_line(data = vocab_full, aes(x = row_number(vocab_full), y = cum_percent, color = "Full Vocab")) +
    geom_line(data = vocab_reduced, aes(x = row_number(vocab_reduced), y = cum_percent, color = "Reduced Vocab")) +
    labs(x = "Dictionary Size", y = "Cumulative % of Instances", 
         title = "Coverage: Full vs. Reduced Vocabulary") +
    scale_color_manual(values = c("Full Vocab" = "blue", "Reduced Vocab" = "red")) +
    theme_minimal()
print(p5)

Plan for Prediction Model and Shiny App

Prediction Algorithm Goals

Develop an efficient, accurate next-word prediction system, informed by exploratory insights, and deploy via Shiny.

Consideration

  1. Efficient Storage (Markov Chains) use SQLite for full trigram model, in-memory subset (e.g., 50k trigrams) for Shiny. Frequency analysis will guide pruning.
  2. Using Word Frequencies for Efficiency Prune low-frequency n-grams (e.g., top 90% cumulative count) and limit vocab to 50%/90% coverage words (from Q3 above), using for others.
  3. N-Gram Model Parameters (Size of n) Stick with n=3, validate with bigram/trigram frequency distributions (Q2). Parameters = unique (prefix, next_word) pairs.Compare bigram vs. trigram frequency tables for sparsity/overlap.
  4. Smoothing Probabilities Add-K smoothing (k=0.01). Test smoothing with 50%/90% vocab sizes, add missing words via backoff.
  5. Model Evaluation Perplexity and accuracy on a 20% test set. Use top frequent n-grams as a benchmark for accuracy.
  6. Backoff Models for Unobserved N-Grams. Stupid Backoff (trigram → bigram → unigram). Build backoff with frequency-weighted discounts.

Prediction Algorithm Plan

Core: Trigram lookup with Add-K smoothing, top-5 predictions. Backoff: Fall back to bigrams/unigrams using frequency data. Storage: SQLite full model, in-memory pruned set.

Shiny App Plan

UI: Text input, top-5 prediction dropdown, stats tab with frequency plots/tables.

Server: Load pruned ngrams, predict reactively, display exploratory plots (e.g., Zipf, coverage curves).

Features: Real-time prediction, toggle for smoothing/backoff options.

Work Completed

Current State: Functional trigram model from 1,000 sentences, expandable to 20,000 (~10%), stored in ngrams table (prefix, next_word, count). Ready for exploratory analysis and prediction development.

LS0tCnRpdGxlOiAiTi1HcmFtIE1vZGVsIERldmVsb3BtZW50IgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIGNvZGVfZm9sZGluZzogaGlkZQotLS0KCiMgUmVwb3J0OiBOLUdyYW0gTW9kZWwgRGV2ZWxvcG1lbnQgYW5kIFByZWRpY3Rpb24gUGxhbgojIyBQcm9qZWN0IE92ZXJ2aWV3ClRoZSBnb2FsIHJlbWFpbnMgdG8gYnVpbGQgYSBuZXh0LXdvcmQgcHJlZGljdGlvbiBtb2RlbCB1c2luZyBhIDYwMCBNQiBFbmdsaXNoIGNvcnB1cyBpbiBlbl9ubHAuZGIsIGFuZCBkZXBsb3kgaXQgdmlhIGEgU2hpbnkgYXBwLiBXZeKAmXZlIGNvbXBsZXRlZCBhIHRyaWdyYW0gbW9kZWwgYW5kIG5vdyBhaW0gdG8gZXhwbG9yZSBpdHMgc3RhdGlzdGljYWwgcHJvcGVydGllcyB0byBvcHRpbWl6ZSB0aGUgcHJlZGljdGlvbiBhbGdvcml0aG0uCgoKNCBTUUxpdGUgZGF0YWJhc2VzIHdlcmUgY3JlYXRlZC4gT25lIGZvciBlYWNoIGxhbmd1YWdlOiBlbmdsaXNoIC0gYGVuX25scC5kYmBldGMuIApgYGAKLS0gZmlsZXMgdGFibGUgb3JpZ2FubHkgaGFkIGxhbmd1YWdlIGJ1dCBmb3Igc2l6ZSBkYXRhYmFzZSB3YXMgc3BsaXQgaW50byBsYW5nYWdlcwpDUkVBVEUgVEFCTEUgZmlsZXMgKAogICAgICBmaWxlX2lkIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT0lOQ1JFTUVOVCwKICAgICAgZmlsZV9uYW1lIFRFWFQsIC0tIGZ1bGwgZmlsZSBwYXRoIAogICAgICBmaWxlX3NpemVfbWIgUkVBTAogICAgKQotLSAgdGV4dCBsb2FkZWQgaW50byBzZW50ZW5jZXMgdGFibGVzCkNSRUFURSBUQUJMRSBzZW50ZW5jZXMgKAogICAgICBzZW50ZW5jZV9pZCBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9JTkNSRU1FTlQsCiAgICAgIGZpbGVfaWQgSU5URUdFUiwKICAgICAgcG9zaXRpb24gSU5URUdFUiwKICAgICAgdGV4dF9jb250ZW50IFRFWFQsIAogICAgICB3b3JkX2NvdW50IElOVEVHRVIsCiAgICAgIEZPUkVJR04gS0VZIChmaWxlX2lkKSBSRUZFUkVOQ0VTIGZpbGVzKGZpbGVfaWQpCiAgICApCi0tIG5ncmFtcyB0YWJsZQpDUkVBVEUgVEFCTEUgbmdyYW1zICgKICAgIHByZWZpeCBURVhULCAKICAgIG5leHRfd29yZCBURVhULCAKICAgIGNvdW50IElOVEVHRVIsIAogICAgUFJJTUFSWSBLRVkgKHByZWZpeCwgbmV4dF93b3JkKQogICAgKQpgYGAKCiMjIEV4cGxvcmF0b3J5IEFuYWx5c2lzIFBsYW4KVG8gcmVmaW5lIHRoZSBtb2RlbCwgd2XigJlsbCBhbmFseXplIHdvcmQgYW5kIG4tZ3JhbSBmcmVxdWVuY2llcywgZGljdGlvbmFyeSBjb3ZlcmFnZSwgZm9yZWlnbiBsYW5ndWFnZSBpbmZsdWVuY2UsIGFuZCBjb3ZlcmFnZSBlbmhhbmNlbWVudCBzdHJhdGVnaWVzLiBCZWxvdyBhcmUgdGhlIG5ldyBxdWVzdGlvbnMsIGFwcHJvYWNoZXMsIGFuZCBwcm9wb3NlZCBvdXRwdXRzLgoKIyMjIERpc3RyaWJ1dGlvbnMgb2Ygd29yZCBmcmVxdWVuY2llcwpgYGB7ciAsICBtZXNzYWdlPUZBTFNFfQpsaWJyYXJ5KFJTUUxpdGUpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkodG9rZW5pemVycykKbGlicmFyeShnZ3Bsb3QyKQoKIyBDb25uZWN0IHRvIGRhdGFiYXNlCmNvbm4gPC0gZGJDb25uZWN0KFNRTGl0ZSgpLCAiZW5fbmxwLmRiIikKCnNhbXBsZV9zZW50ZW5jZXMgPC0gZnVuY3Rpb24oY29ubiwgc2FtcGxlX3NpemUpIHsKICAgIHF1ZXJ5IDwtICIKICAgICAgICBTRUxFQ1QgQ0FTVCh0ZXh0X2NvbnRlbnQgQVMgVEVYVCkgQVMgc2VudGVuY2UgCiAgICAgICAgRlJPTSBzZW50ZW5jZXMgCiAgICAgICAgV0hFUkUgc2VudGVuY2VfaWQgSU4gKAogICAgICAgICAgICBTRUxFQ1Qgc2VudGVuY2VfaWQgCiAgICAgICAgICAgIEZST00gc2VudGVuY2VzIAogICAgICAgICAgICBPUkRFUiBCWSBSQU5ET00oKSAKICAgICAgICAgICAgTElNSVQgPwogICAgICAgICkiCiAgICBkYXRhIDwtIGRiR2V0UXVlcnkoY29ubiwgcXVlcnksIHBhcmFtcyA9IGxpc3Qoc2FtcGxlX3NpemUpKQogICAgZGF0YSRzZW50ZW5jZSA8LSBhcy5jaGFyYWN0ZXIoZGF0YSRzZW50ZW5jZSkKICAgIGRhdGEkc2VudGVuY2VbaXMubmEoZGF0YSRzZW50ZW5jZSldIDwtICIiCiAgICBkYXRhCn0KCiMgU2FtcGxlIDEwMCwwMDAgc2VudGVuY2VzCnNhbXBsZV9zaXplIDwtIDEwMDAwMApzYW1wbGVkX3NlbnRlbmNlcyA8LSBzYW1wbGVfc2VudGVuY2VzKGNvbm4sIHNhbXBsZV9zaXplKQpjYXQoIlNhbXBsZWQiLCBucm93KHNhbXBsZWRfc2VudGVuY2VzKSwgInNlbnRlbmNlc1xuIikKCiMgVG9rZW5pemUgYWxsIHNlbnRlbmNlcyBpbnRvIHdvcmRzCndvcmRzX2xpc3QgPC0gbGFwcGx5KHNhbXBsZWRfc2VudGVuY2VzJHNlbnRlbmNlLCB0b2tlbml6ZV93b3JkcywgbG93ZXJjYXNlID0gVFJVRSwgc3RyaXBfcHVuY3QgPSBUUlVFKQp3b3JkcyA8LSB1bmxpc3Qod29yZHNfbGlzdCkKCiMgQ291bnQgd29yZCBmcmVxdWVuY2llcwp3b3JkX2ZyZXEgPC0gdGFibGUod29yZHMpICU+JSBhcy5kYXRhLmZyYW1lKCkgJT4lIAogICAgcmVuYW1lKHdvcmQgPSB3b3JkcywgZnJlcSA9IEZyZXEpICU+JSAKICAgIGFycmFuZ2UoZGVzYyhmcmVxKSkgJT4lIAogICAgbXV0YXRlKHJhbmsgPSByb3dfbnVtYmVyKCkpCgojIFBsb3Q6IExvZy1Mb2cgV29yZCBGcmVxdWVuY3kgdnMuIFJhbmsgKFppcGbigJlzIExhdykKcDEgPC0gZ2dwbG90KHdvcmRfZnJlcSwgYWVzKHggPSByYW5rLCB5ID0gZnJlcSkpICsKICAgIGdlb21fbGluZSgpICsKICAgIHNjYWxlX3hfbG9nMTAoIlJhbmsgKGxvZyBzY2FsZSkiKSArCiAgICBzY2FsZV95X2xvZzEwKCJGcmVxdWVuY3kgKGxvZyBzY2FsZSkiKSArCiAgICBnZ3RpdGxlKCJXb3JkIEZyZXF1ZW5jeSBEaXN0cmlidXRpb24gKFppcGbigJlzIExhdykiKSArCiAgICB0aGVtZV9taW5pbWFsKCkKcHJpbnQocDEpCgojIFRhYmxlOiBUb3AgMTAgRnJlcXVlbnQgV29yZHMKdG9wX3dvcmRzIDwtIGhlYWQod29yZF9mcmVxLCAxMCkgJT4lIHNlbGVjdCh3b3JkLCBmcmVxKQpwcmludCh0b3Bfd29yZHMpCmBgYAoKIyMjIEZyZXF1ZW5jaWVzIG9mIDItR3JhbXMgYW5kIDMtR3JhbXMKYGBge3IgfQojIEZ1bmN0aW9ucyBmb3IgYmlncmFtIGFuZCB0cmlncmFtIGV4dHJhY3Rpb24KZXh0cmFjdF9iaWdyYW1zIDwtIGZ1bmN0aW9uKHNlbnRlbmNlKSB7CiAgICB3b3JkcyA8LSB0b2tlbml6ZV93b3JkcyhzZW50ZW5jZSwgbG93ZXJjYXNlID0gVFJVRSwgc3RyaXBfcHVuY3QgPSBUUlVFKVtbMV1dCiAgICBpZiAobGVuZ3RoKHdvcmRzKSA8IDIpIHJldHVybihkYXRhLmZyYW1lKHByZWZpeCA9IGNoYXJhY3RlcigpLCBuZXh0X3dvcmQgPSBjaGFyYWN0ZXIoKSkpCiAgICBkYXRhLmZyYW1lKAogICAgICAgIHByZWZpeCA9IHdvcmRzWzE6KGxlbmd0aCh3b3JkcyktMSldLAogICAgICAgIG5leHRfd29yZCA9IHdvcmRzWzI6bGVuZ3RoKHdvcmRzKV0KICAgICkKfQoKZXh0cmFjdF90cmlncmFtcyA8LSBmdW5jdGlvbihzZW50ZW5jZSkgewogICAgd29yZHMgPC0gdG9rZW5pemVfd29yZHMoc2VudGVuY2UsIGxvd2VyY2FzZSA9IFRSVUUsIHN0cmlwX3B1bmN0ID0gVFJVRSlbWzFdXQogICAgaWYgKGxlbmd0aCh3b3JkcykgPCAzKSByZXR1cm4oZGF0YS5mcmFtZShwcmVmaXggPSBjaGFyYWN0ZXIoKSwgbmV4dF93b3JkID0gY2hhcmFjdGVyKCkpKQogICAgbiA8LSBsZW5ndGgod29yZHMpIC0gMgogICAgZGF0YS5mcmFtZSgKICAgICAgICBwcmVmaXggPSBzYXBwbHkoMTpuLCBmdW5jdGlvbihpKSBwYXN0ZSh3b3Jkc1tpXSwgd29yZHNbaSsxXSwgc2VwPSIgIikpLAogICAgICAgIG5leHRfd29yZCA9IHdvcmRzWygxOm4pICsgMl0KICAgICkKfQoKIyBHZW5lcmF0ZSBiaWdyYW1zIGFuZCB0cmlncmFtcyB3aXRoIHByb2dyZXNzIGJhcgpiaWdyYW1zX2xpc3QgPC0gbGlzdCgpCnRyaWdyYW1zX2xpc3QgPC0gbGlzdCgpCmZvciAoaSBpbiAxOm5yb3coc2FtcGxlZF9zZW50ZW5jZXMpKSB7CiAgICBzZW50ZW5jZSA8LSBzYW1wbGVkX3NlbnRlbmNlcyRzZW50ZW5jZVtpXQogICAgYmlncmFtc19saXN0W1tpXV0gPC0gZXh0cmFjdF9iaWdyYW1zKHNlbnRlbmNlKQogICAgdHJpZ3JhbXNfbGlzdFtbaV1dIDwtIGV4dHJhY3RfdHJpZ3JhbXMoc2VudGVuY2UpCn0KCiMgQ29tYmluZSBhbmQgY291bnQKYmlncmFtcyA8LSBiaW5kX3Jvd3MoYmlncmFtc19saXN0KSAlPiUgCiAgICBncm91cF9ieShwcmVmaXgsIG5leHRfd29yZCkgJT4lIAogICAgc3VtbWFyaXNlKGZyZXEgPSBuKCksIC5ncm91cHMgPSAiZHJvcCIpICU+JSAKICAgIGFycmFuZ2UoZGVzYyhmcmVxKSkKdHJpZ3JhbXMgPC0gYmluZF9yb3dzKHRyaWdyYW1zX2xpc3QpICU+JSAKICAgIGdyb3VwX2J5KHByZWZpeCwgbmV4dF93b3JkKSAlPiUgCiAgICBzdW1tYXJpc2UoZnJlcSA9IG4oKSwgLmdyb3VwcyA9ICJkcm9wIikgJT4lIAogICAgYXJyYW5nZShkZXNjKGZyZXEpKQoKIyBQbG90OiBUb3AgMjAgQmlncmFtcyBhbmQgVHJpZ3JhbXMgQmFyIFBsb3QKdG9wX2JpZ3JhbXMgPC0gaGVhZChiaWdyYW1zLCAyMCkgJT4lIG11dGF0ZShuX2dyYW0gPSBwYXN0ZShwcmVmaXgsIG5leHRfd29yZCkpCnRvcF90cmlncmFtcyA8LSBoZWFkKHRyaWdyYW1zLCAyMCkgJT4lIG11dGF0ZShuX2dyYW0gPSBwYXN0ZShwcmVmaXgsIG5leHRfd29yZCkpCnAyIDwtIGdncGxvdChiaW5kX3Jvd3MobXV0YXRlKHRvcF9iaWdyYW1zLCB0eXBlID0gIkJpZ3JhbSIpLCBtdXRhdGUodG9wX3RyaWdyYW1zLCB0eXBlID0gIlRyaWdyYW0iKSksIAogICAgICAgICAgICAgYWVzKHggPSByZW9yZGVyKG5fZ3JhbSwgZnJlcSksIHkgPSBmcmVxLCBmaWxsID0gdHlwZSkpICsKICAgIGdlb21fYmFyKHN0YXQgPSAiaWRlbnRpdHkiKSArCiAgICBjb29yZF9mbGlwKCkgKwogICAgbGFicyh4ID0gIk4tR3JhbSIsIHkgPSAiRnJlcXVlbmN5IiwgdGl0bGUgPSAiVG9wIDIwIEJpZ3JhbXMgYW5kIFRyaWdyYW1zIikgKwogICAgZmFjZXRfd3JhcCh+dHlwZSwgc2NhbGVzID0gImZyZWVfeSIpICsKICAgIHRoZW1lX21pbmltYWwoKQpwcmludChwMikKCiMgVGFibGU6IE4tR3JhbSBGcmVxdWVuY3kgU3VtbWFyeQpuZ3JhbV9zdW1tYXJ5IDwtIGRhdGEuZnJhbWUoCiAgICAiTi1HcmFtIFR5cGUiID0gYygiQmlncmFtIiwgIlRyaWdyYW0iKSwKICAgICJVbmlxdWUgQ291bnQiID0gYyhucm93KGJpZ3JhbXMpLCBucm93KHRyaWdyYW1zKSksCiAgICAiQXZnIEZyZXF1ZW5jeSIgPSBjKG1lYW4oYmlncmFtcyRmcmVxKSwgbWVhbih0cmlncmFtcyRmcmVxKSksCiAgICAiTWF4IEZyZXF1ZW5jeSIgPSBjKG1heChiaWdyYW1zJGZyZXEpLCBtYXgodHJpZ3JhbXMkZnJlcSkpCikKcHJpbnQoIk4tR3JhbSBGcmVxdWVuY3kgU3VtbWFyeToiKQpwcmludChuZ3JhbV9zdW1tYXJ5KQpgYGAKCiMjIyBEaWN0aW9uYXJ5IENvdmVyYWdlICg1MCUgYW5kIDkwJSkKYGBge3IgfQojIENhbGN1bGF0ZSBjdW11bGF0aXZlIGNvdmVyYWdlCnRvdGFsX2luc3RhbmNlcyA8LSBzdW0od29yZF9mcmVxJGZyZXEpCndvcmRfZnJlcSA8LSB3b3JkX2ZyZXEgJT4lIAogICAgbXV0YXRlKGN1bV9mcmVxID0gY3Vtc3VtKGZyZXEpLCAKICAgICAgICAgICBjdW1fcGVyY2VudCA9IGN1bV9mcmVxIC8gdG90YWxfaW5zdGFuY2VzICogMTAwKQpjdXRvZmZfNTAgPC0gd29yZF9mcmVxICU+JSBmaWx0ZXIoY3VtX3BlcmNlbnQgPj0gNTApICU+JSBzbGljZSgxKQpjdXRvZmZfOTAgPC0gd29yZF9mcmVxICU+JSBmaWx0ZXIoY3VtX3BlcmNlbnQgPj0gOTApICU+JSBzbGljZSgxKQoKIyBQbG90OiBDdW11bGF0aXZlIEZyZXF1ZW5jeSBDdXJ2ZQpwMyA8LSBnZ3Bsb3Qod29yZF9mcmVxLCBhZXMoeCA9IHJhbmssIHkgPSBjdW1fcGVyY2VudCkpICsKICAgIGdlb21fbGluZSgpICsKICAgIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IGN1dG9mZl81MCRyYW5rLCBsaW5ldHlwZSA9ICJkYXNoZWQiLCBjb2xvciA9ICJibHVlIikgKwogICAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gY3V0b2ZmXzkwJHJhbmssIGxpbmV0eXBlID0gImRhc2hlZCIsIGNvbG9yID0gInJlZCIpICsKICAgIGxhYnMoeCA9ICJOdW1iZXIgb2YgVW5pcXVlIFdvcmRzIiwgeSA9ICJDdW11bGF0aXZlICUgb2YgSW5zdGFuY2VzIiwgCiAgICAgICAgIHRpdGxlID0gIkRpY3Rpb25hcnkgQ292ZXJhZ2UiKSArCiAgICBhbm5vdGF0ZSgidGV4dCIsIHggPSBjdXRvZmZfNTAkcmFuaywgeSA9IDYwLCBsYWJlbCA9ICI1MCUiLCBjb2xvciA9ICJibHVlIikgKwogICAgYW5ub3RhdGUoInRleHQiLCB4ID0gY3V0b2ZmXzkwJHJhbmssIHkgPSA5NSwgbGFiZWwgPSAiOTAlIiwgY29sb3IgPSAicmVkIikgKwogICAgdGhlbWVfbWluaW1hbCgpCnByaW50KHAzKQojZ2dzYXZlKCJjb3ZlcmFnZV9jdXJ2ZS5wbmciLCBwMywgd2lkdGggPSA4LCBoZWlnaHQgPSA2KQoKIyBUYWJsZTogQ292ZXJhZ2UgVGhyZXNob2xkcwpjb3ZlcmFnZV90YWJsZSA8LSBkYXRhLmZyYW1lKAogICAgQ292ZXJhZ2UgPSBjKCI1MCUiLCAiOTAlIiksCiAgICAiVW5pcXVlIFdvcmRzIiA9IGMoY3V0b2ZmXzUwJHJhbmssIGN1dG9mZl85MCRyYW5rKSwKICAgICJFeGFtcGxlIFdvcmRzIiA9IGMoY3V0b2ZmXzUwJHdvcmQsIGN1dG9mZl85MCR3b3JkKQopCmBgYAoKIyMjIEZvcmVpZ24gTGFuZ3VhZ2UgV29yZHMKUHJ1bmluZyB3b3JkcyB3aXRoIGxlc3MgdGhhbiA1IGZyZXF1ZW5jeSBzZWVtIHRvIGltcHJvdmUgImZvcmVpZ24iIHdvcmRzIGRldGVjdGlvbiAoZXhwZWN0ZWQgNSUtMTAlKS4KSG93ZXZlciAiZm9yZWlnbiIgaXMgbWlzbGVhZGluZyBhcyB0aGUgdG9wIGZvcmVpZ24gd29yZHMgYXJlIG1pc3NwZWxsZWQgRW5nbGlzaCB3b3Jkcy4KSSB1c2VkIFNDT1dMIHRvIG1hdGNoIEVuZ2xpc2ggd29yZHMuIFRva2VuaXppbmcgd29yZHMgaW50byBzdGVtcyBwcm92ZWQgYmVuZWZpY2lhbC4KCmBgYHtyfQojIFRva2VuaXplIGFuZCBjbGVhbiB3aXRoIHJlZ2V4CndvcmRzX2xpc3QgPC0gbGFwcGx5KHNhbXBsZWRfc2VudGVuY2VzJHNlbnRlbmNlLCB0b2tlbml6ZV93b3JkX3N0ZW1zLCBsYW5ndWFnZSA9ICJlbmdsaXNoIikKd29yZHMgPC0gdW5saXN0KHdvcmRzX2xpc3QpCndvcmRzX2NsZWFuIDwtIGdzdWIoIltbOnB1bmN0Ol1dfFswLTldIiwgIiIsIHdvcmRzKQoKd29yZF9mcmVxIDwtIHRhYmxlKHdvcmRzX2NsZWFuKSAlPiUgYXMuZGF0YS5mcmFtZSgpICU+JSAKICAgIHJlbmFtZSh3b3JkID0gd29yZHNfY2xlYW4sIGZyZXEgPSBGcmVxKSAlPiUgCiAgICBhcnJhbmdlKGRlc2MoZnJlcSkpICU+JSAKICAgIG11dGF0ZShyYW5rID0gcm93X251bWJlcigpKQoKIyBMb2FkIFNDT1dMIEVuZ2xpc2ggd29yZCBsaXN0CmVuZ2xpc2hfd29yZHMgPC0gcmVhZExpbmVzKCJzY293bF93b3Jkcy50eHQiLCBlbmNvZGluZyA9ICJVVEYtOCIsIHdhcm4gPSBGQUxTRSkKIyBBZGQgbWluaW1hbCBleHRyYXMgKHNsYW5nLCBhYmJyZXZpYXRpb25zKQpleHRyYV93b3JkcyA8LSBjKCJkb250IiwgIndvbnQiLCAiY2FudCIsICJnb25uYSIsICJ5YWxsIiwgImFpbnQiLCAiaW0iLCAiaXZlIiwgImlkIiwgInlvdXJlIiwgCiAgICAgICAgICAgICAgICAgIml0cyIsICJ0aGV5cmUiLCAid2FzbnQiLCAiaXNudCIsICJkaWRudCIsICJ0aGF0cyIsICJoZXJlcyIsICJ0aGVyZXMiLAogICAgICAgICAgICAgICAgICJibG9nIiwgInJ0IiwgImxvbCIsICJoYWhhIiwgImRyIikKZW5nbGlzaF93b3Jkc19jbGVhbiA8LSBjKGVuZ2xpc2hfd29yZHMsIGV4dHJhX3dvcmRzKQoKIyBDbGFzc2lmeSB3b3Jkcwp3b3JkX2ZyZXEkd29yZCA8LSBhcy5jaGFyYWN0ZXIod29yZF9mcmVxJHdvcmQpCndvcmRfZnJlcSRpc19lbmdsaXNoIDwtIHdvcmRfZnJlcSR3b3JkICVpbiUgZW5nbGlzaF93b3Jkc19jbGVhbgoKZm9yZWlnbl93b3JkcyA8LSB3b3JkX2ZyZXEgJT4lIGZpbHRlcighaXNfZW5nbGlzaCkKCiMgUHJvcG9ydGlvbiBjaGVjayAodW5maWx0ZXJlZCkKZW5nbGlzaF9jb3VudCA8LSBzdW0od29yZF9mcmVxJGZyZXFbd29yZF9mcmVxJGlzX2VuZ2xpc2hdKQpmb3JlaWduX2NvdW50IDwtIHN1bSh3b3JkX2ZyZXEkZnJlcVshd29yZF9mcmVxJGlzX2VuZ2xpc2hdKQpjYXQoIkVuZ2xpc2ggaW5zdGFuY2VzOiIsIGVuZ2xpc2hfY291bnQsICJcbiIpCmNhdCgiRm9yZWlnbiBpbnN0YW5jZXM6IiwgZm9yZWlnbl9jb3VudCwgIlxuIikKY2F0KCJGb3JlaWduIHByb3BvcnRpb246IiwgZm9yZWlnbl9jb3VudCAvIChlbmdsaXNoX2NvdW50ICsgZm9yZWlnbl9jb3VudCkgKiAxMDAsICIlXG4iKQoKIyBGaWx0ZXIgaGlnaC1mcmVxdWVuY3kgImZvcmVpZ24iIHdvcmRzCmZvcmVpZ25fd29yZHNfZmlsdGVyZWQgPC0gZm9yZWlnbl93b3JkcyAlPiUgZmlsdGVyKGZyZXEgPCA1KQoKIyBQcm9wb3J0aW9uIGNoZWNrIChmaWx0ZXJlZCkKZW5nbGlzaF9jb3VudF9maWx0ZXJlZCA8LSBlbmdsaXNoX2NvdW50ICsgc3VtKGZvcmVpZ25fd29yZHMkZnJlcVtmb3JlaWduX3dvcmRzJGZyZXEgPj0gNV0pCmZvcmVpZ25fY291bnRfZmlsdGVyZWQgPC0gc3VtKGZvcmVpZ25fd29yZHNfZmlsdGVyZWQkZnJlcSkKY2F0KCJFbmdsaXNoIGluc3RhbmNlcyAocHJ1bmVkKToiLCBlbmdsaXNoX2NvdW50X2ZpbHRlcmVkLCAiXG4iKQpjYXQoIkZvcmVpZ24gaW5zdGFuY2VzIChwcnVuZWQpOiIsIGZvcmVpZ25fY291bnRfZmlsdGVyZWQsICJcbiIpCmNhdCgiRm9yZWlnbiBwcm9wb3J0aW9uIChwcnVuZWQpOiIsIGZvcmVpZ25fY291bnRfZmlsdGVyZWQgLyAoZW5nbGlzaF9jb3VudF9maWx0ZXJlZCArIGZvcmVpZ25fY291bnRfZmlsdGVyZWQpICogMTAwLCAiJVxuIikKCiMgUGxvdDogUGllIENoYXJ0IChmaWx0ZXJlZCkKcGllX2RhdGEgPC0gZGF0YS5mcmFtZShjYXRlZ29yeSA9IGMoIkVuZ2xpc2giLCAiRm9yZWlnbiIpLCAKICAgICAgICAgICAgICAgICAgICAgICBjb3VudCA9IGMoZW5nbGlzaF9jb3VudF9maWx0ZXJlZCwgZm9yZWlnbl9jb3VudF9maWx0ZXJlZCkpCnA0IDwtIGdncGxvdChwaWVfZGF0YSwgYWVzKHggPSAiIiwgeSA9IGNvdW50LCBmaWxsID0gY2F0ZWdvcnkpKSArCiAgICBnZW9tX2JhcihzdGF0ID0gImlkZW50aXR5Iiwgd2lkdGggPSAxKSArCiAgICBjb29yZF9wb2xhcigieSIpICsKICAgIGxhYnModGl0bGUgPSAiRW5nbGlzaCB2cy4gRm9yZWlnbiBXb3JkIEluc3RhbmNlcyAoRmlsdGVyZWQpIikgKwogICAgdGhlbWVfdm9pZCgpICsKICAgIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcyA9IGMoIkVuZ2xpc2giID0gImJsdWUiLCAiRm9yZWlnbiIgPSAicmVkIikpCnByaW50KHA0KQoKIyBUYWJsZTogVG9wIDEwIFN1c3BlY3RlZCBGb3JlaWduIFdvcmRzCnRvcF9mb3JlaWduIDwtIGhlYWQoZm9yZWlnbl93b3Jkc19maWx0ZXJlZCwgMTApICU+JSBzZWxlY3Qod29yZCwgZnJlcSkKcHJpbnQoIlRvcCAxMCBTdXNwZWN0ZWQgRm9yZWlnbiBXb3JkczoiKQpwcmludCh0b3BfZm9yZWlnbikKYGBgCgoKCiMjIEluY3JlYXNpbmcgQ292ZXJhZ2UKVXNpbmcgPFVOSz4gZm9yIHJhcmUgd29yZHMgKGZyZXEgPCA1KSwgcmVkdWNpbmcgdm9jYWJ1bGFyeSBzaXplIHdoaWxlIG1haW50YWluaW5nIHBocmFzZSBjb3ZlcmFnZS4KCmBgYHtyIH0KIyBTaW11bGF0ZSBjbHVzdGVyaW5nIChzaW1wbGlmaWVkOiBhc3N1bWUgcmFyZSB3b3JkcyBncm91cGVkIGFzIDxVTks+KQp2b2NhYl9mdWxsIDwtIHdvcmRfZnJlcSAlPiUKICAgIGdyb3VwX2J5KHdvcmQpICU+JSAKICAgIHN1bW1hcmlzZShmcmVxID0gc3VtKGZyZXEpKSAlPiUgCiAgICBhcnJhbmdlKGRlc2MoZnJlcSkpICU+JSAKICAgIG11dGF0ZShjdW1fZnJlcSA9IGN1bXN1bShmcmVxKSwgY3VtX3BlcmNlbnQgPSBjdW1fZnJlcSAvIHRvdGFsX2luc3RhbmNlcyAqIDEwMCkKCnZvY2FiX3JlZHVjZWQgPC0gd29yZF9mcmVxICU+JSAKICAgIG11dGF0ZSh3b3JkID0gaWZlbHNlKGZyZXEgPCA1LCAiPFVOSz4iLCB3b3JkKSkgJT4lIAogICAgZ3JvdXBfYnkod29yZCkgJT4lIAogICAgc3VtbWFyaXNlKGZyZXEgPSBzdW0oZnJlcSkpICU+JSAKICAgIGFycmFuZ2UoZGVzYyhmcmVxKSkgJT4lIAogICAgbXV0YXRlKGN1bV9mcmVxID0gY3Vtc3VtKGZyZXEpLCBjdW1fcGVyY2VudCA9IGN1bV9mcmVxIC8gdG90YWxfaW5zdGFuY2VzICogMTAwKQoKIyBQbG90OiBDb3ZlcmFnZSB2cy4gRGljdGlvbmFyeSBTaXplCnA1IDwtIGdncGxvdCgpICsKICAgIGdlb21fbGluZShkYXRhID0gdm9jYWJfZnVsbCwgYWVzKHggPSByb3dfbnVtYmVyKHZvY2FiX2Z1bGwpLCB5ID0gY3VtX3BlcmNlbnQsIGNvbG9yID0gIkZ1bGwgVm9jYWIiKSkgKwogICAgZ2VvbV9saW5lKGRhdGEgPSB2b2NhYl9yZWR1Y2VkLCBhZXMoeCA9IHJvd19udW1iZXIodm9jYWJfcmVkdWNlZCksIHkgPSBjdW1fcGVyY2VudCwgY29sb3IgPSAiUmVkdWNlZCBWb2NhYiIpKSArCiAgICBsYWJzKHggPSAiRGljdGlvbmFyeSBTaXplIiwgeSA9ICJDdW11bGF0aXZlICUgb2YgSW5zdGFuY2VzIiwgCiAgICAgICAgIHRpdGxlID0gIkNvdmVyYWdlOiBGdWxsIHZzLiBSZWR1Y2VkIFZvY2FidWxhcnkiKSArCiAgICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzID0gYygiRnVsbCBWb2NhYiIgPSAiYmx1ZSIsICJSZWR1Y2VkIFZvY2FiIiA9ICJyZWQiKSkgKwogICAgdGhlbWVfbWluaW1hbCgpCnByaW50KHA1KQpgYGAKCiMgUGxhbiBmb3IgUHJlZGljdGlvbiBNb2RlbCBhbmQgU2hpbnkgQXBwCgojIyMgUHJlZGljdGlvbiBBbGdvcml0aG0gR29hbHMKRGV2ZWxvcCBhbiBlZmZpY2llbnQsIGFjY3VyYXRlIG5leHQtd29yZCBwcmVkaWN0aW9uIHN5c3RlbSwgaW5mb3JtZWQgYnkgZXhwbG9yYXRvcnkgaW5zaWdodHMsIGFuZCBkZXBsb3kgdmlhIFNoaW55LgoKIyMgQ29uc2lkZXJhdGlvbiAKCjEuICoqRWZmaWNpZW50IFN0b3JhZ2UgKE1hcmtvdiBDaGFpbnMpKiogdXNlIFNRTGl0ZSBmb3IgZnVsbCB0cmlncmFtIG1vZGVsLCBpbi1tZW1vcnkgc3Vic2V0IChlLmcuLCA1MGsgdHJpZ3JhbXMpIGZvciBTaGlueS4gRnJlcXVlbmN5IGFuYWx5c2lzIHdpbGwgZ3VpZGUgcHJ1bmluZy4KMi4gKipVc2luZyBXb3JkIEZyZXF1ZW5jaWVzIGZvciBFZmZpY2llbmN5KiogIFBydW5lIGxvdy1mcmVxdWVuY3kgbi1ncmFtcyAoZS5nLiwgdG9wIDkwJSBjdW11bGF0aXZlIGNvdW50KSBhbmQgbGltaXQgdm9jYWIgdG8gNTAlLzkwJSBjb3ZlcmFnZSB3b3JkcyAoZnJvbSBRMyBhYm92ZSksIHVzaW5nIDxVTks+IGZvciBvdGhlcnMuCjMuICoqTi1HcmFtIE1vZGVsIFBhcmFtZXRlcnMgKFNpemUgb2YgbikqKiBTdGljayB3aXRoIG49MywgdmFsaWRhdGUgd2l0aCBiaWdyYW0vdHJpZ3JhbSBmcmVxdWVuY3kgZGlzdHJpYnV0aW9ucyAoUTIpLiBQYXJhbWV0ZXJzID0gdW5pcXVlIChwcmVmaXgsIG5leHRfd29yZCkgcGFpcnMuQ29tcGFyZSBiaWdyYW0gdnMuIHRyaWdyYW0gZnJlcXVlbmN5IHRhYmxlcyBmb3Igc3BhcnNpdHkvb3ZlcmxhcC4KNC4gKipTbW9vdGhpbmcgUHJvYmFiaWxpdGllcyoqIEFkZC1LIHNtb290aGluZyAoaz0wLjAxKS4gVGVzdCBzbW9vdGhpbmcgd2l0aCA1MCUvOTAlIHZvY2FiIHNpemVzLCBhZGQgbWlzc2luZyB3b3JkcyB2aWEgYmFja29mZi4KNS4gKipNb2RlbCBFdmFsdWF0aW9uKiogUGVycGxleGl0eSBhbmQgYWNjdXJhY3kgb24gYSAyMCUgdGVzdCBzZXQuIFVzZSB0b3AgZnJlcXVlbnQgbi1ncmFtcyBhcyBhIGJlbmNobWFyayBmb3IgYWNjdXJhY3kuCjYuICoqQmFja29mZiBNb2RlbHMqKiBmb3IgVW5vYnNlcnZlZCBOLUdyYW1zLiBTdHVwaWQgQmFja29mZiAodHJpZ3JhbSDihpIgYmlncmFtIOKGkiB1bmlncmFtKS4gQnVpbGQgYmFja29mZiB3aXRoIGZyZXF1ZW5jeS13ZWlnaHRlZCBkaXNjb3VudHMuCgojIyBQcmVkaWN0aW9uIEFsZ29yaXRobSBQbGFuCgpDb3JlOiBUcmlncmFtIGxvb2t1cCB3aXRoIEFkZC1LIHNtb290aGluZywgdG9wLTUgcHJlZGljdGlvbnMuCkJhY2tvZmY6IEZhbGwgYmFjayB0byBiaWdyYW1zL3VuaWdyYW1zIHVzaW5nIGZyZXF1ZW5jeSBkYXRhLgpTdG9yYWdlOiBTUUxpdGUgZnVsbCBtb2RlbCwgaW4tbWVtb3J5IHBydW5lZCBzZXQuCgojIyBTaGlueSBBcHAgUGxhbgoKKipVSToqKiBUZXh0IGlucHV0LCB0b3AtNSBwcmVkaWN0aW9uIGRyb3Bkb3duLCBzdGF0cyB0YWIgd2l0aCBmcmVxdWVuY3kgcGxvdHMvdGFibGVzLgoKKipTZXJ2ZXI6KiogTG9hZCBwcnVuZWQgbmdyYW1zLCBwcmVkaWN0IHJlYWN0aXZlbHksIGRpc3BsYXkgZXhwbG9yYXRvcnkgcGxvdHMgKGUuZy4sIFppcGYsIGNvdmVyYWdlIGN1cnZlcykuCgoqKkZlYXR1cmVzOioqIFJlYWwtdGltZSBwcmVkaWN0aW9uLCB0b2dnbGUgZm9yIHNtb290aGluZy9iYWNrb2ZmIG9wdGlvbnMuCgojIyBXb3JrIENvbXBsZXRlZApDdXJyZW50IFN0YXRlOiBGdW5jdGlvbmFsIHRyaWdyYW0gbW9kZWwgZnJvbSAxLDAwMCBzZW50ZW5jZXMsIGV4cGFuZGFibGUgdG8gMjAsMDAwICh+MTAlKSwgc3RvcmVkIGluIG5ncmFtcyB0YWJsZSAocHJlZml4LCBuZXh0X3dvcmQsIGNvdW50KS4gUmVhZHkgZm9yIGV4cGxvcmF0b3J5IGFuYWx5c2lzIGFuZCBwcmVkaWN0aW9uIGRldmVsb3BtZW50Lg==