style: Hash less stuff in the bloom filter, using the precomputed hashes we have.

This commit is contained in:
Emilio Cobos Álvarez 2017-03-21 19:38:16 +01:00
parent 65ebbb7c56
commit e29b84de18
No known key found for this signature in database
GPG key ID: 056B727BB9C1027C
10 changed files with 129 additions and 63 deletions

2
Cargo.lock generated
View file

@ -2392,6 +2392,7 @@ dependencies = [
"cssparser 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "cssparser 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"precomputed-hash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]] [[package]]
@ -2753,6 +2754,7 @@ dependencies = [
"ordered-float 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "ordered-float 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "parking_lot 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pdqsort 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "pdqsort 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"precomputed-hash 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rayon 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"selectors 0.18.0", "selectors 0.18.0",

View file

@ -20,3 +20,4 @@ bitflags = "0.7"
matches = "0.1" matches = "0.1"
cssparser = "0.12.1" cssparser = "0.12.1"
fnv = "1.0" fnv = "1.0"
precomputed-hash = "0.1"

View file

@ -6,6 +6,7 @@
#[macro_use] extern crate cssparser; #[macro_use] extern crate cssparser;
#[macro_use] extern crate matches; #[macro_use] extern crate matches;
extern crate fnv; extern crate fnv;
extern crate precomputed_hash;
pub mod bloom; pub mod bloom;
pub mod matching; pub mod matching;

View file

@ -4,6 +4,7 @@
use bloom::BloomFilter; use bloom::BloomFilter;
use parser::{CaseSensitivity, Combinator, ComplexSelector, LocalName}; use parser::{CaseSensitivity, Combinator, ComplexSelector, LocalName};
use parser::{SimpleSelector, Selector, SelectorImpl}; use parser::{SimpleSelector, Selector, SelectorImpl};
use precomputed_hash::PrecomputedHash;
use std::borrow::Borrow; use std::borrow::Borrow;
use tree::Element; use tree::Element;
@ -135,23 +136,23 @@ fn may_match<E>(mut selector: &ComplexSelector<E::Impl>,
for ss in selector.compound_selector.iter() { for ss in selector.compound_selector.iter() {
match *ss { match *ss {
SimpleSelector::LocalName(LocalName { ref name, ref lower_name }) => { SimpleSelector::LocalName(LocalName { ref name, ref lower_name }) => {
if !bf.might_contain(name) && if !bf.might_contain_hash(name.precomputed_hash()) &&
!bf.might_contain(lower_name) { !bf.might_contain_hash(lower_name.precomputed_hash()) {
return false return false
} }
}, },
SimpleSelector::Namespace(ref namespace) => { SimpleSelector::Namespace(ref namespace) => {
if !bf.might_contain(&namespace.url) { if !bf.might_contain_hash(namespace.url.precomputed_hash()) {
return false return false
} }
}, },
SimpleSelector::ID(ref id) => { SimpleSelector::ID(ref id) => {
if !bf.might_contain(id) { if !bf.might_contain_hash(id.precomputed_hash()) {
return false return false
} }
}, },
SimpleSelector::Class(ref class) => { SimpleSelector::Class(ref class) => {
if !bf.might_contain(class) { if !bf.might_contain_hash(class.precomputed_hash()) {
return false return false
} }
}, },

View file

@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use cssparser::{Token, Parser as CssParser, parse_nth, ToCss, serialize_identifier, CssStringWriter}; use cssparser::{Token, Parser as CssParser, parse_nth, ToCss, serialize_identifier, CssStringWriter};
use precomputed_hash::PrecomputedHash;
use std::ascii::AsciiExt; use std::ascii::AsciiExt;
use std::borrow::{Borrow, Cow}; use std::borrow::{Borrow, Cow};
use std::cmp; use std::cmp;
@ -39,10 +40,10 @@ macro_rules! with_all_bounds {
/// of pseudo-classes/elements /// of pseudo-classes/elements
pub trait SelectorImpl: Sized { pub trait SelectorImpl: Sized {
type AttrValue: $($InSelector)*; type AttrValue: $($InSelector)*;
type Identifier: $($InSelector)*; type Identifier: $($InSelector)* + PrecomputedHash;
type ClassName: $($InSelector)*; type ClassName: $($InSelector)* + PrecomputedHash;
type LocalName: $($InSelector)* + Borrow<Self::BorrowedLocalName>; type LocalName: $($InSelector)* + Borrow<Self::BorrowedLocalName> + PrecomputedHash;
type NamespaceUrl: $($CommonBounds)* + Default + Borrow<Self::BorrowedNamespaceUrl>; type NamespaceUrl: $($CommonBounds)* + Default + Borrow<Self::BorrowedNamespaceUrl> + PrecomputedHash;
type NamespacePrefix: $($InSelector)* + Default; type NamespacePrefix: $($InSelector)* + Default;
type BorrowedNamespaceUrl: ?Sized + Eq; type BorrowedNamespaceUrl: ?Sized + Eq;
type BorrowedLocalName: ?Sized + Eq + Hash; type BorrowedLocalName: ?Sized + Eq + Hash;
@ -1184,24 +1185,49 @@ pub mod tests {
#[derive(Default)] #[derive(Default)]
pub struct DummyParser { pub struct DummyParser {
default_ns: Option<String>, default_ns: Option<DummyAtom>,
ns_prefixes: HashMap<String, String>, ns_prefixes: HashMap<DummyAtom, DummyAtom>,
} }
impl SelectorImpl for DummySelectorImpl { impl SelectorImpl for DummySelectorImpl {
type AttrValue = String; type AttrValue = DummyAtom;
type Identifier = String; type Identifier = DummyAtom;
type ClassName = String; type ClassName = DummyAtom;
type LocalName = String; type LocalName = DummyAtom;
type NamespaceUrl = String; type NamespaceUrl = DummyAtom;
type NamespacePrefix = String; type NamespacePrefix = DummyAtom;
type BorrowedLocalName = str; type BorrowedLocalName = DummyAtom;
type BorrowedNamespaceUrl = str; type BorrowedNamespaceUrl = DummyAtom;
type NonTSPseudoClass = PseudoClass; type NonTSPseudoClass = PseudoClass;
type PseudoElement = PseudoElement; type PseudoElement = PseudoElement;
} }
type ShutUpTidy = String; #[derive(Default, Debug, Hash, Clone, PartialEq, Eq)]
pub struct DummyAtom(String);
impl fmt::Display for DummyAtom {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
<String as fmt::Display>::fmt(&self.0, fmt)
}
}
impl From<String> for DummyAtom {
fn from(string: String) -> Self {
DummyAtom(string)
}
}
impl<'a> From<&'a str> for DummyAtom {
fn from(string: &'a str) -> Self {
DummyAtom(string.into())
}
}
impl PrecomputedHash for DummyAtom {
fn precomputed_hash(&self) -> u32 {
return 0
}
}
impl Parser for DummyParser { impl Parser for DummyParser {
type Impl = DummySelectorImpl; type Impl = DummySelectorImpl;
@ -1232,11 +1258,11 @@ pub mod tests {
} }
} }
fn default_namespace(&self) -> Option<String> { fn default_namespace(&self) -> Option<DummyAtom> {
self.default_ns.clone() self.default_ns.clone()
} }
fn namespace_for_prefix(&self, prefix: &ShutUpTidy) -> Option<String> { fn namespace_for_prefix(&self, prefix: &DummyAtom) -> Option<DummyAtom> {
self.ns_prefixes.get(prefix).cloned() self.ns_prefixes.get(prefix).cloned()
} }
} }
@ -1276,8 +1302,8 @@ pub mod tests {
assert_eq!(parse("EeÉ"), Ok(SelectorList(vec!(Selector { assert_eq!(parse("EeÉ"), Ok(SelectorList(vec!(Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::LocalName(LocalName { compound_selector: vec!(SimpleSelector::LocalName(LocalName {
name: String::from("EeÉ"), name: DummyAtom::from("EeÉ"),
lower_name: String::from("eeÉ") })), lower_name: DummyAtom::from("eeÉ") })),
next: None, next: None,
}), }),
pseudo_element: None, pseudo_element: None,
@ -1286,7 +1312,7 @@ pub mod tests {
assert_eq!(parse(".foo:lang(en-US)"), Ok(SelectorList(vec!(Selector { assert_eq!(parse(".foo:lang(en-US)"), Ok(SelectorList(vec!(Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec![ compound_selector: vec![
SimpleSelector::Class(String::from("foo")), SimpleSelector::Class(DummyAtom::from("foo")),
SimpleSelector::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned())) SimpleSelector::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned()))
], ],
next: None, next: None,
@ -1296,7 +1322,7 @@ pub mod tests {
})))); }))));
assert_eq!(parse("#bar"), Ok(SelectorList(vec!(Selector { assert_eq!(parse("#bar"), Ok(SelectorList(vec!(Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::ID(String::from("bar"))), compound_selector: vec!(SimpleSelector::ID(DummyAtom::from("bar"))),
next: None, next: None,
}), }),
pseudo_element: None, pseudo_element: None,
@ -1305,10 +1331,10 @@ pub mod tests {
assert_eq!(parse("e.foo#bar"), Ok(SelectorList(vec!(Selector { assert_eq!(parse("e.foo#bar"), Ok(SelectorList(vec!(Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::LocalName(LocalName { compound_selector: vec!(SimpleSelector::LocalName(LocalName {
name: String::from("e"), name: DummyAtom::from("e"),
lower_name: String::from("e") }), lower_name: DummyAtom::from("e") }),
SimpleSelector::Class(String::from("foo")), SimpleSelector::Class(DummyAtom::from("foo")),
SimpleSelector::ID(String::from("bar"))), SimpleSelector::ID(DummyAtom::from("bar"))),
next: None, next: None,
}), }),
pseudo_element: None, pseudo_element: None,
@ -1316,12 +1342,12 @@ pub mod tests {
})))); }))));
assert_eq!(parse("e.foo #bar"), Ok(SelectorList(vec!(Selector { assert_eq!(parse("e.foo #bar"), Ok(SelectorList(vec!(Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::ID(String::from("bar"))), compound_selector: vec!(SimpleSelector::ID(DummyAtom::from("bar"))),
next: Some((Arc::new(ComplexSelector { next: Some((Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::LocalName(LocalName { compound_selector: vec!(SimpleSelector::LocalName(LocalName {
name: String::from("e"), name: DummyAtom::from("e"),
lower_name: String::from("e") }), lower_name: DummyAtom::from("e") }),
SimpleSelector::Class(String::from("foo"))), SimpleSelector::Class(DummyAtom::from("foo"))),
next: None, next: None,
}), Combinator::Descendant)), }), Combinator::Descendant)),
}), }),
@ -1334,8 +1360,8 @@ pub mod tests {
assert_eq!(parse_ns("[Foo]", &parser), Ok(SelectorList(vec!(Selector { assert_eq!(parse_ns("[Foo]", &parser), Ok(SelectorList(vec!(Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::AttrExists(AttrSelector { compound_selector: vec!(SimpleSelector::AttrExists(AttrSelector {
name: String::from("Foo"), name: DummyAtom::from("Foo"),
lower_name: String::from("foo"), lower_name: DummyAtom::from("foo"),
namespace: NamespaceConstraint::Specific(Namespace { namespace: NamespaceConstraint::Specific(Namespace {
prefix: None, prefix: None,
url: "".into(), url: "".into(),
@ -1347,17 +1373,17 @@ pub mod tests {
specificity: specificity(0, 1, 0), specificity: specificity(0, 1, 0),
})))); }))));
assert_eq!(parse_ns("svg|circle", &parser), Err(())); assert_eq!(parse_ns("svg|circle", &parser), Err(()));
parser.ns_prefixes.insert("svg".into(), SVG.into()); parser.ns_prefixes.insert(DummyAtom("svg".into()), DummyAtom(SVG.into()));
assert_eq!(parse_ns("svg|circle", &parser), Ok(SelectorList(vec![Selector { assert_eq!(parse_ns("svg|circle", &parser), Ok(SelectorList(vec![Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec![ compound_selector: vec![
SimpleSelector::Namespace(Namespace { SimpleSelector::Namespace(Namespace {
prefix: Some("svg".into()), prefix: Some(DummyAtom("svg".into())),
url: SVG.into(), url: SVG.into(),
}), }),
SimpleSelector::LocalName(LocalName { SimpleSelector::LocalName(LocalName {
name: String::from("circle"), name: DummyAtom::from("circle"),
lower_name: String::from("circle") lower_name: DummyAtom::from("circle"),
}) })
], ],
next: None, next: None,
@ -1378,8 +1404,8 @@ pub mod tests {
url: MATHML.into(), url: MATHML.into(),
}), }),
SimpleSelector::AttrExists(AttrSelector { SimpleSelector::AttrExists(AttrSelector {
name: String::from("Foo"), name: DummyAtom::from("Foo"),
lower_name: String::from("foo"), lower_name: DummyAtom::from("foo"),
namespace: NamespaceConstraint::Specific(Namespace { namespace: NamespaceConstraint::Specific(Namespace {
prefix: None, prefix: None,
url: "".into(), url: "".into(),
@ -1400,8 +1426,8 @@ pub mod tests {
url: MATHML.into(), url: MATHML.into(),
}), }),
SimpleSelector::LocalName(LocalName { SimpleSelector::LocalName(LocalName {
name: String::from("e"), name: DummyAtom::from("e"),
lower_name: String::from("e") }), lower_name: DummyAtom::from("e") }),
), ),
next: None, next: None,
}), }),
@ -1412,13 +1438,13 @@ pub mod tests {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec![ compound_selector: vec![
SimpleSelector::AttrDashMatch(AttrSelector { SimpleSelector::AttrDashMatch(AttrSelector {
name: String::from("attr"), name: DummyAtom::from("attr"),
lower_name: String::from("attr"), lower_name: DummyAtom::from("attr"),
namespace: NamespaceConstraint::Specific(Namespace { namespace: NamespaceConstraint::Specific(Namespace {
prefix: None, prefix: None,
url: "".into(), url: "".into(),
}), }),
}, "foo".to_owned()) }, DummyAtom::from("foo"))
], ],
next: None, next: None,
}), }),
@ -1441,8 +1467,8 @@ pub mod tests {
compound_selector: vec!(), compound_selector: vec!(),
next: Some((Arc::new(ComplexSelector { next: Some((Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::LocalName(LocalName { compound_selector: vec!(SimpleSelector::LocalName(LocalName {
name: String::from("div"), name: DummyAtom::from("div"),
lower_name: String::from("div") })), lower_name: DummyAtom::from("div") })),
next: None, next: None,
}), Combinator::Descendant)), }), Combinator::Descendant)),
}), }),
@ -1452,11 +1478,11 @@ pub mod tests {
assert_eq!(parse("#d1 > .ok"), Ok(SelectorList(vec![Selector { assert_eq!(parse("#d1 > .ok"), Ok(SelectorList(vec![Selector {
complex_selector: Arc::new(ComplexSelector { complex_selector: Arc::new(ComplexSelector {
compound_selector: vec![ compound_selector: vec![
SimpleSelector::Class(String::from("ok")), SimpleSelector::Class(DummyAtom::from("ok")),
], ],
next: Some((Arc::new(ComplexSelector { next: Some((Arc::new(ComplexSelector {
compound_selector: vec![ compound_selector: vec![
SimpleSelector::ID(String::from("d1")), SimpleSelector::ID(DummyAtom::from("d1")),
], ],
next: None, next: None,
}), Combinator::Child)), }), Combinator::Child)),
@ -1469,13 +1495,13 @@ pub mod tests {
compound_selector: vec!(SimpleSelector::Negation( compound_selector: vec!(SimpleSelector::Negation(
vec!( vec!(
Arc::new(ComplexSelector { Arc::new(ComplexSelector {
compound_selector: vec!(SimpleSelector::Class(String::from("babybel"))), compound_selector: vec!(SimpleSelector::Class(DummyAtom::from("babybel"))),
next: None next: None
}), }),
Arc::new(ComplexSelector { Arc::new(ComplexSelector {
compound_selector: vec!( compound_selector: vec!(
SimpleSelector::ID(String::from("provel")), SimpleSelector::ID(DummyAtom::from("provel")),
SimpleSelector::Class(String::from("old")), SimpleSelector::Class(DummyAtom::from("old")),
), ),
next: None next: None
}), }),

View file

@ -44,6 +44,7 @@ num-traits = "0.1.32"
ordered-float = "0.4" ordered-float = "0.4"
parking_lot = "0.3.3" parking_lot = "0.3.3"
pdqsort = "0.1.0" pdqsort = "0.1.0"
precomputed-hash = "0.1"
rayon = "0.6" rayon = "0.6"
selectors = { path = "../selectors" } selectors = { path = "../selectors" }
serde = {version = "0.9", optional = true} serde = {version = "0.9", optional = true}

View file

@ -10,6 +10,7 @@ use gecko_bindings::bindings::Gecko_AddRefAtom;
use gecko_bindings::bindings::Gecko_Atomize; use gecko_bindings::bindings::Gecko_Atomize;
use gecko_bindings::bindings::Gecko_ReleaseAtom; use gecko_bindings::bindings::Gecko_ReleaseAtom;
use gecko_bindings::structs::nsIAtom; use gecko_bindings::structs::nsIAtom;
use precomputed_hash::PrecomputedHash;
use std::borrow::{Cow, Borrow}; use std::borrow::{Cow, Borrow};
use std::char::{self, DecodeUtf16}; use std::char::{self, DecodeUtf16};
use std::fmt::{self, Write}; use std::fmt::{self, Write};
@ -56,6 +57,13 @@ impl Deref for Atom {
} }
} }
impl PrecomputedHash for Atom {
#[inline]
fn precomputed_hash(&self) -> u32 {
self.get_hash()
}
}
impl Borrow<WeakAtom> for Atom { impl Borrow<WeakAtom> for Atom {
#[inline] #[inline]
fn borrow(&self) -> &WeakAtom { fn borrow(&self) -> &WeakAtom {

View file

@ -5,6 +5,7 @@
//! A type to represent a namespace. //! A type to represent a namespace.
use gecko_bindings::structs::nsIAtom; use gecko_bindings::structs::nsIAtom;
use precomputed_hash::PrecomputedHash;
use std::borrow::{Borrow, Cow}; use std::borrow::{Borrow, Cow};
use std::fmt; use std::fmt;
use std::ops::Deref; use std::ops::Deref;
@ -19,10 +20,27 @@ macro_rules! ns {
#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)] #[derive(Debug, PartialEq, Eq, Clone, Default, Hash)]
pub struct Namespace(pub Atom); pub struct Namespace(pub Atom);
impl PrecomputedHash for Namespace {
#[inline]
fn precomputed_hash(&self) -> u32 {
self.0.precomputed_hash()
}
}
/// A Gecko WeakNamespace is a wrapped WeakAtom. /// A Gecko WeakNamespace is a wrapped WeakAtom.
#[derive(Hash)] #[derive(Hash)]
pub struct WeakNamespace(WeakAtom); pub struct WeakNamespace(WeakAtom);
impl Deref for WeakNamespace {
type Target = WeakAtom;
#[inline]
fn deref(&self) -> &WeakAtom {
&self.0
}
}
impl Deref for Namespace { impl Deref for Namespace {
type Target = WeakNamespace; type Target = WeakNamespace;

View file

@ -66,6 +66,7 @@ extern crate num_traits;
extern crate ordered_float; extern crate ordered_float;
extern crate parking_lot; extern crate parking_lot;
extern crate pdqsort; extern crate pdqsort;
#[cfg(feature = "gecko")] extern crate precomputed_hash;
extern crate rayon; extern crate rayon;
extern crate selectors; extern crate selectors;
#[cfg(feature = "servo")] #[macro_use] extern crate serde_derive; #[cfg(feature = "servo")] #[macro_use] extern crate serde_derive;

View file

@ -1175,23 +1175,30 @@ pub trait MatchMethods : TElement {
/// Therefore, each node must have its matching selectors inserted _after_ /// Therefore, each node must have its matching selectors inserted _after_
/// its own selector matching and _before_ its children start. /// its own selector matching and _before_ its children start.
fn insert_into_bloom_filter(&self, bf: &mut BloomFilter) { fn insert_into_bloom_filter(&self, bf: &mut BloomFilter) {
bf.insert(&*self.get_local_name()); bf.insert_hash(self.get_local_name().get_hash());
bf.insert(&*self.get_namespace()); bf.insert_hash(self.get_namespace().get_hash());
self.get_id().map(|id| bf.insert(&id)); if let Some(id) = self.get_id() {
bf.insert_hash(id.get_hash());
}
// TODO: case-sensitivity depends on the document type and quirks mode // TODO: case-sensitivity depends on the document type and quirks mode
self.each_class(|class| bf.insert(class)); self.each_class(|class| {
bf.insert_hash(class.get_hash())
});
} }
/// After all the children are done css selector matching, this must be /// After all the children are done css selector matching, this must be
/// called to reset the bloom filter after an `insert`. /// called to reset the bloom filter after an `insert`.
fn remove_from_bloom_filter(&self, bf: &mut BloomFilter) { fn remove_from_bloom_filter(&self, bf: &mut BloomFilter) {
bf.remove(&*self.get_local_name()); bf.remove_hash(self.get_local_name().get_hash());
bf.remove(&*self.get_namespace()); bf.remove_hash(self.get_namespace().get_hash());
self.get_id().map(|id| bf.remove(&id)); if let Some(id) = self.get_id() {
bf.remove_hash(id.get_hash());
}
// TODO: case-sensitivity depends on the document type and quirks mode // TODO: case-sensitivity depends on the document type and quirks mode
self.each_class(|class| bf.remove(class)); self.each_class(|class| {
bf.remove_hash(class.get_hash())
});
} }
/// Given the old and new style of this element, and whether it's a /// Given the old and new style of this element, and whether it's a