Merge branch 'perf_improvement'
This commit is contained in:
commit
44f7412a5c
@ -59,6 +59,7 @@ default = []
|
||||
compare = ["tokio/process", "tokio/macros"]
|
||||
foreign_document_test = ["compare", "dep:futures", "tokio/sync", "dep:walkdir", "tokio/process"]
|
||||
tracing = ["dep:opentelemetry", "dep:opentelemetry-otlp", "dep:opentelemetry-semantic-conventions", "dep:tokio", "dep:tracing", "dep:tracing-opentelemetry", "dep:tracing-subscriber"]
|
||||
event_count = []
|
||||
|
||||
# Optimized build for any sort of release.
|
||||
[profile.release-lto]
|
||||
|
@ -14,7 +14,7 @@ function main {
|
||||
additional_flags+=(--profile "$PROFILE")
|
||||
fi
|
||||
(cd "$DIR/../" && cargo build --no-default-features "${additional_flags[@]}")
|
||||
perf record --freq=2000 --call-graph dwarf --output="$DIR/../perf.data" "$DIR/../target/${PROFILE}/parse" "${@}"
|
||||
perf record --freq=70000 --call-graph dwarf --output="$DIR/../perf.data" "$DIR/../target/${PROFILE}/parse" "${@}"
|
||||
|
||||
# Convert to a format firefox will read
|
||||
# flags to consider --show-info
|
||||
|
@ -10,7 +10,6 @@ pub enum CustomError {
|
||||
Text(String),
|
||||
Static(&'static str),
|
||||
IO(std::io::Error),
|
||||
BoxedError(Box<dyn std::error::Error>),
|
||||
Parser(ErrorKind),
|
||||
}
|
||||
|
||||
@ -37,8 +36,8 @@ impl From<&'static str> for CustomError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error>> for CustomError {
|
||||
fn from(value: Box<dyn std::error::Error>) -> Self {
|
||||
CustomError::BoxedError(value)
|
||||
impl From<String> for CustomError {
|
||||
fn from(value: String) -> Self {
|
||||
CustomError::Text(value)
|
||||
}
|
||||
}
|
||||
|
43
src/event_count/database.rs
Normal file
43
src/event_count/database.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::EventType;
|
||||
use crate::parser::OrgSource;
|
||||
|
||||
#[derive(Debug, Eq, Hash, PartialEq)]
|
||||
struct EventKey {
|
||||
event_type: EventType,
|
||||
byte_offset: usize,
|
||||
}
|
||||
|
||||
pub(crate) type EventCount = usize;
|
||||
|
||||
static GLOBAL_DATA: Mutex<Option<HashMap<EventKey, EventCount>>> = Mutex::new(None);
|
||||
|
||||
pub(crate) fn record_event(event_type: EventType, input: OrgSource<'_>) {
|
||||
let mut db = GLOBAL_DATA.lock().unwrap();
|
||||
let db = db.get_or_insert_with(HashMap::new);
|
||||
let key = EventKey {
|
||||
event_type,
|
||||
byte_offset: input.get_byte_offset(),
|
||||
};
|
||||
*db.entry(key).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
pub fn report(original_document: &str) {
|
||||
let mut db = GLOBAL_DATA.lock().unwrap();
|
||||
let db = db.get_or_insert_with(HashMap::new);
|
||||
let mut results: Vec<_> = db.iter().map(|(k, v)| (k, v)).collect();
|
||||
results.sort_by_key(|(_k, v)| *v);
|
||||
// This would put the most common at the top, but that is a pain when there is already a lot of output from the parser.
|
||||
// results.sort_by(|(_ak, av), (_bk, bv)| bv.cmp(av));
|
||||
for (key, count) in results {
|
||||
println!(
|
||||
"{:?} {} character offset: {} byte offset: {}",
|
||||
key.event_type,
|
||||
count,
|
||||
original_document[..key.byte_offset].chars().count() + 1,
|
||||
key.byte_offset
|
||||
)
|
||||
}
|
||||
}
|
4
src/event_count/event_type.rs
Normal file
4
src/event_count/event_type.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#[derive(Debug, Eq, Hash, PartialEq)]
|
||||
pub(crate) enum EventType {
|
||||
ElementStart,
|
||||
}
|
6
src/event_count/mod.rs
Normal file
6
src/event_count/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
mod database;
|
||||
mod event_type;
|
||||
|
||||
pub(crate) use database::record_event;
|
||||
pub use database::report;
|
||||
pub(crate) use event_type::EventType;
|
@ -13,6 +13,8 @@ pub mod compare;
|
||||
|
||||
mod context;
|
||||
mod error;
|
||||
#[cfg(feature = "event_count")]
|
||||
pub mod event_count;
|
||||
mod iter;
|
||||
pub mod parser;
|
||||
pub mod types;
|
||||
|
@ -54,8 +54,11 @@ fn read_stdin_to_string() -> Result<String, Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
fn run_anonymous_parse<P: AsRef<str>>(org_contents: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rust_parsed = parse(org_contents.as_ref())?;
|
||||
let org_contents = org_contents.as_ref();
|
||||
let rust_parsed = parse(org_contents)?;
|
||||
println!("{:#?}", rust_parsed);
|
||||
#[cfg(feature = "event_count")]
|
||||
organic::event_count::report(org_contents);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -75,5 +78,7 @@ fn run_parse_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::err
|
||||
};
|
||||
let rust_parsed = parse_with_settings(org_contents, &global_settings)?;
|
||||
println!("{:#?}", rust_parsed);
|
||||
#[cfg(feature = "event_count")]
|
||||
organic::event_count::report(org_contents);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -14,17 +14,48 @@ use nom::multi::many0;
|
||||
use nom::multi::many_till;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::keyword::affiliated_keyword;
|
||||
use super::object_parser::standard_set_object;
|
||||
use super::util::confine_context;
|
||||
use super::OrgSource;
|
||||
use crate::context::bind_context;
|
||||
use crate::context::Context;
|
||||
use crate::context::ContextElement;
|
||||
use crate::context::GlobalSettings;
|
||||
use crate::context::List;
|
||||
use crate::context::RefContext;
|
||||
use crate::error::Res;
|
||||
use crate::types::AffiliatedKeywordValue;
|
||||
use crate::types::AffiliatedKeywords;
|
||||
use crate::types::Keyword;
|
||||
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(ret, level = "debug", skip(context))
|
||||
)]
|
||||
pub(crate) fn affiliated_keywords<'b, 'g, 'r, 's>(
|
||||
context: RefContext<'b, 'g, 'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Vec<Keyword<'s>>> {
|
||||
let mut ret = Vec::new();
|
||||
let mut remaining = input;
|
||||
|
||||
loop {
|
||||
let result = affiliated_keyword(context, remaining);
|
||||
match result {
|
||||
Ok((remain, kw)) => {
|
||||
remaining = remain;
|
||||
ret.push(kw);
|
||||
}
|
||||
Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((remaining, ret))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_affiliated_keywords<'g, 's, AK>(
|
||||
global_settings: &'g GlobalSettings<'g, 's>,
|
||||
input: AK,
|
||||
|
@ -129,23 +129,12 @@ fn document_org_source<'b, 'g, 'r, 's>(
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
for setup_file in setup_files.iter().map(String::as_str) {
|
||||
let (_, setup_file_settings) =
|
||||
scan_for_in_buffer_settings(setup_file.into()).map_err(|err| {
|
||||
eprintln!("{}", err);
|
||||
nom::Err::Error(CustomError::Static(
|
||||
"TODO: make this take an owned string so I can dump err.to_string() into it.",
|
||||
))
|
||||
})?;
|
||||
let (_, setup_file_settings) = scan_for_in_buffer_settings(setup_file.into())?;
|
||||
final_settings.extend(setup_file_settings);
|
||||
}
|
||||
final_settings.extend(document_settings);
|
||||
let new_settings = apply_in_buffer_settings(final_settings, context.get_global_settings())
|
||||
.map_err(|err| {
|
||||
eprintln!("{}", err);
|
||||
nom::Err::Error(CustomError::Static(
|
||||
"TODO: make this take an owned string so I can dump err.to_string() into it.",
|
||||
))
|
||||
})?;
|
||||
.map_err(nom::Err::Error)?;
|
||||
let new_context = context.with_global_settings(&new_settings);
|
||||
let context = &new_context;
|
||||
|
||||
@ -170,15 +159,13 @@ fn document_org_source<'b, 'g, 'r, 's>(
|
||||
let parser_context = context.with_global_settings(&new_global_settings);
|
||||
let (remaining, mut document) = _document(&parser_context, input)
|
||||
.map(|(rem, out)| (Into::<&str>::into(rem), out))?;
|
||||
apply_post_parse_in_buffer_settings(&mut document)
|
||||
.map_err(|err| nom::Err::<CustomError>::Failure(err.into()))?;
|
||||
apply_post_parse_in_buffer_settings(&mut document);
|
||||
return Ok((remaining.into(), document));
|
||||
}
|
||||
}
|
||||
|
||||
// Find final in-buffer settings that do not impact parsing
|
||||
apply_post_parse_in_buffer_settings(&mut document)
|
||||
.map_err(|err| nom::Err::<CustomError>::Failure(err.into()))?;
|
||||
apply_post_parse_in_buffer_settings(&mut document);
|
||||
|
||||
Ok((remaining.into(), document))
|
||||
}
|
||||
@ -208,3 +195,17 @@ fn _document<'b, 'g, 'r, 's>(
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use test::Bencher;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[bench]
|
||||
fn bench_full_document(b: &mut Bencher) {
|
||||
let input = include_str!("../../org_mode_samples/element_container_priority/README.org");
|
||||
|
||||
b.iter(|| assert!(parse(input).is_ok()));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
use nom::multi::many0;
|
||||
|
||||
use super::babel_call::babel_call;
|
||||
use super::clock::clock;
|
||||
use super::comment::comment;
|
||||
@ -14,7 +12,6 @@ use super::footnote_definition::detect_footnote_definition;
|
||||
use super::footnote_definition::footnote_definition;
|
||||
use super::greater_block::greater_block;
|
||||
use super::horizontal_rule::horizontal_rule;
|
||||
use super::keyword::affiliated_keyword;
|
||||
use super::keyword::keyword;
|
||||
use super::latex_environment::latex_environment;
|
||||
use super::lesser_block::comment_block;
|
||||
@ -27,10 +24,14 @@ use super::paragraph::paragraph;
|
||||
use super::plain_list::detect_plain_list;
|
||||
use super::plain_list::plain_list;
|
||||
use super::table::detect_table;
|
||||
use crate::context::parser_with_context;
|
||||
use crate::context::RefContext;
|
||||
use crate::error::CustomError;
|
||||
use crate::error::Res;
|
||||
#[cfg(feature = "event_count")]
|
||||
use crate::event_count::record_event;
|
||||
#[cfg(feature = "event_count")]
|
||||
use crate::event_count::EventType;
|
||||
use crate::parser::affiliated_keyword::affiliated_keywords;
|
||||
use crate::parser::macros::ak_element;
|
||||
use crate::parser::macros::element;
|
||||
use crate::parser::table::org_mode_table;
|
||||
@ -54,8 +55,10 @@ fn _element<'b, 'g, 'r, 's>(
|
||||
input: OrgSource<'s>,
|
||||
can_be_paragraph: bool,
|
||||
) -> Res<OrgSource<'s>, Element<'s>> {
|
||||
#[cfg(feature = "event_count")]
|
||||
record_event(EventType::ElementStart, input);
|
||||
let (post_affiliated_keywords_input, affiliated_keywords) =
|
||||
many0(parser_with_context!(affiliated_keyword)(context))(input)?;
|
||||
affiliated_keywords(context, input)?;
|
||||
|
||||
let mut affiliated_keywords = affiliated_keywords.into_iter();
|
||||
|
||||
@ -270,7 +273,7 @@ fn _detect_element<'b, 'g, 'r, 's>(
|
||||
can_be_paragraph: bool,
|
||||
) -> Res<OrgSource<'s>, ()> {
|
||||
let (post_affiliated_keywords_input, affiliated_keywords) =
|
||||
many0(parser_with_context!(affiliated_keyword)(context))(input)?;
|
||||
affiliated_keywords(context, input)?;
|
||||
|
||||
let mut affiliated_keywords = affiliated_keywords.into_iter();
|
||||
|
||||
|
@ -18,12 +18,13 @@ use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::section::section;
|
||||
use super::util::exit_matcher_parser;
|
||||
use super::util::get_consumed;
|
||||
use super::util::org_line_ending;
|
||||
use super::util::org_space;
|
||||
use super::util::org_space_or_line_ending;
|
||||
use super::util::start_of_line;
|
||||
use crate::context::parser_with_context;
|
||||
use crate::context::bind_context;
|
||||
use crate::context::ContextElement;
|
||||
use crate::context::ExitClass;
|
||||
use crate::context::ExitMatcherNode;
|
||||
@ -61,10 +62,10 @@ fn _heading<'b, 'g, 'r, 's>(
|
||||
let mut scheduled = None;
|
||||
let mut deadline = None;
|
||||
let mut closed = None;
|
||||
not(|i| context.check_exit_matcher(i))(input)?;
|
||||
not(bind_context!(exit_matcher_parser, context))(input)?;
|
||||
let (remaining, pre_headline) = headline(context, input, parent_star_count)?;
|
||||
let section_matcher = parser_with_context!(section)(context);
|
||||
let heading_matcher = parser_with_context!(heading(pre_headline.star_count))(context);
|
||||
let section_matcher = bind_context!(section, context);
|
||||
let heading_matcher = bind_context!(heading(pre_headline.star_count), context);
|
||||
let (remaining, maybe_section) =
|
||||
opt(map(section_matcher, DocumentElement::Section))(remaining)?;
|
||||
let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?;
|
||||
@ -154,7 +155,7 @@ fn headline<'b, 'g, 'r, 's>(
|
||||
let (remaining, (_, (headline_level, star_count, _), _)) = tuple((
|
||||
start_of_line,
|
||||
verify(
|
||||
parser_with_context!(headline_level)(&parser_context),
|
||||
bind_context!(headline_level, &parser_context),
|
||||
|(_, count, _)| *count > parent_star_count,
|
||||
),
|
||||
peek(org_space),
|
||||
@ -162,7 +163,7 @@ fn headline<'b, 'g, 'r, 's>(
|
||||
|
||||
let (remaining, maybe_todo_keyword) = opt(tuple((
|
||||
space1,
|
||||
parser_with_context!(heading_keyword)(&parser_context),
|
||||
bind_context!(heading_keyword, &parser_context),
|
||||
peek(org_space_or_line_ending),
|
||||
)))(remaining)?;
|
||||
|
||||
@ -176,9 +177,7 @@ fn headline<'b, 'g, 'r, 's>(
|
||||
|
||||
let (remaining, maybe_title) = opt(tuple((
|
||||
space1,
|
||||
consumed(many1(parser_with_context!(standard_set_object)(
|
||||
&parser_context,
|
||||
))),
|
||||
consumed(many1(bind_context!(standard_set_object, &parser_context))),
|
||||
)))(remaining)?;
|
||||
|
||||
let (remaining, maybe_tags) = opt(tuple((space0, tags)))(remaining)?;
|
||||
|
@ -88,7 +88,7 @@ fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSou
|
||||
pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
|
||||
keywords: Vec<Keyword<'sf>>,
|
||||
original_settings: &'g GlobalSettings<'g, 's>,
|
||||
) -> Result<GlobalSettings<'g, 's>, String> {
|
||||
) -> Result<GlobalSettings<'g, 's>, CustomError> {
|
||||
let mut new_settings = original_settings.clone();
|
||||
|
||||
// Todo Keywords
|
||||
@ -98,7 +98,11 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
|
||||
|| kw.key.eq_ignore_ascii_case("typ_todo")
|
||||
}) {
|
||||
let (_, (in_progress_words, complete_words)) =
|
||||
todo_keywords(kw.value).map_err(|err| err.to_string())?;
|
||||
todo_keywords(kw.value).map_err(|err| match err {
|
||||
nom::Err::Incomplete(_) => CustomError::Text(err.to_string()),
|
||||
nom::Err::Error(e) => e,
|
||||
nom::Err::Failure(e) => e,
|
||||
})?;
|
||||
new_settings
|
||||
.in_progress_todo_keywords
|
||||
.extend(in_progress_words.into_iter().map(str::to_string));
|
||||
@ -112,9 +116,14 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
|
||||
.iter()
|
||||
.filter(|kw| kw.key.eq_ignore_ascii_case("startup"))
|
||||
{
|
||||
let (_remaining, settings) =
|
||||
separated_list0(space1::<&str, nom::error::Error<_>>, is_not(" \t"))(kw.value)
|
||||
.map_err(|err: nom::Err<_>| err.to_string())?;
|
||||
let (_remaining, settings) = separated_list0(space1::<&str, CustomError>, is_not(" \t"))(
|
||||
kw.value,
|
||||
)
|
||||
.map_err(|err: nom::Err<_>| match err {
|
||||
nom::Err::Incomplete(_) => CustomError::Text(err.to_string()),
|
||||
nom::Err::Error(e) => e,
|
||||
nom::Err::Failure(e) => e,
|
||||
})?;
|
||||
if settings.contains(&"odd") {
|
||||
new_settings.odd_levels_only = HeadlineLevelFilter::Odd;
|
||||
}
|
||||
@ -128,7 +137,11 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
|
||||
.iter()
|
||||
.filter(|kw| kw.key.eq_ignore_ascii_case("link"))
|
||||
{
|
||||
let (_, (link_key, link_value)) = link_template(kw.value).map_err(|e| e.to_string())?;
|
||||
let (_, (link_key, link_value)) = link_template(kw.value).map_err(|err| match err {
|
||||
nom::Err::Incomplete(_) => CustomError::Text(err.to_string()),
|
||||
nom::Err::Error(e) => e,
|
||||
nom::Err::Failure(e) => e,
|
||||
})?;
|
||||
new_settings
|
||||
.link_templates
|
||||
.insert(link_key.to_owned(), link_value.to_owned());
|
||||
@ -139,9 +152,7 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
|
||||
|
||||
/// Apply in-buffer settings that do not impact parsing and therefore can be applied after parsing.
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(
|
||||
document: &mut Document<'s>,
|
||||
) -> Result<(), &'static str> {
|
||||
pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(document: &mut Document<'s>) {
|
||||
document.category = Into::<AstNode>::into(&*document)
|
||||
.into_iter()
|
||||
.filter_map(|ast_node| {
|
||||
@ -154,7 +165,6 @@ pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(
|
||||
})
|
||||
.last()
|
||||
.map(|kw| kw.value.to_owned());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
|
@ -4,11 +4,9 @@ use nom::bytes::complete::tag;
|
||||
use nom::bytes::complete::tag_no_case;
|
||||
use nom::bytes::complete::take_while1;
|
||||
use nom::character::complete::anychar;
|
||||
use nom::character::complete::line_ending;
|
||||
use nom::character::complete::one_of;
|
||||
use nom::character::complete::space0;
|
||||
use nom::combinator::consumed;
|
||||
use nom::combinator::eof;
|
||||
use nom::combinator::map;
|
||||
use nom::combinator::not;
|
||||
use nom::combinator::peek;
|
||||
@ -22,7 +20,8 @@ use super::org_source::BracketDepth;
|
||||
use super::org_source::OrgSource;
|
||||
use super::util::get_consumed;
|
||||
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
|
||||
use crate::context::parser_with_context;
|
||||
use super::util::org_line_ending;
|
||||
use crate::context::bind_context;
|
||||
use crate::context::RefContext;
|
||||
use crate::error::CustomError;
|
||||
use crate::error::Res;
|
||||
@ -49,9 +48,8 @@ fn _filtered_keyword<'s, F: Fn(OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s
|
||||
// TODO: When key is a member of org-element-parsed-keywords, value can contain the standard set objects, excluding footnote references.
|
||||
let (remaining, (consumed_input, (_, _, parsed_key, _))) =
|
||||
consumed(tuple((space0, tag("#+"), key_parser, tag(":"))))(input)?;
|
||||
if let Ok((remaining, _)) =
|
||||
tuple((space0::<_, CustomError>, alt((line_ending, eof))))(remaining)
|
||||
{
|
||||
let (remaining, _ws) = space0(remaining)?;
|
||||
if let Ok((remaining, _)) = org_line_ending(remaining) {
|
||||
return Ok((
|
||||
remaining,
|
||||
Keyword {
|
||||
@ -62,12 +60,9 @@ fn _filtered_keyword<'s, F: Fn(OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s
|
||||
},
|
||||
));
|
||||
}
|
||||
let (remaining, _ws) = space0(remaining)?;
|
||||
let (remaining, parsed_value) = recognize(many_till(
|
||||
anychar,
|
||||
peek(tuple((space0, alt((line_ending, eof))))),
|
||||
))(remaining)?;
|
||||
let (remaining, _ws) = tuple((space0, alt((line_ending, eof))))(remaining)?;
|
||||
let (remaining, parsed_value) =
|
||||
recognize(many_till(anychar, peek(tuple((space0, org_line_ending)))))(remaining)?;
|
||||
let (remaining, _ws) = tuple((space0, org_line_ending))(remaining)?;
|
||||
Ok((
|
||||
remaining,
|
||||
Keyword {
|
||||
@ -107,7 +102,7 @@ pub(crate) fn affiliated_keyword<'b, 'g, 'r, 's>(
|
||||
context: RefContext<'b, 'g, 'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Keyword<'s>> {
|
||||
filtered_keyword(parser_with_context!(affiliated_key)(context))(input)
|
||||
filtered_keyword(bind_context!(affiliated_key, context))(input)
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
|
Loading…
x
Reference in New Issue
Block a user